ui/base/identifier/README.md
Unique identifiers are values which:
constexprintptr_t is atomicstd::set and std::mapA unique identifier variable can be empty, or it can be assigned to an existing, known identifier.
MyIdentifier id; // Empty identifier.
CHECK(!id); // This is a valid check.
id = kMyIdentifierValue; // Assign an identifier constant.
CHECK(!!id); // Valid id is truthy.
CHECK_NE(id, kMyOtherIdentifierValue); // Id constants are distinct.
The underlying type of all identifiers is UniqueIdentifier. However you will
not directly use this class. Instead, you create a strongly-typed identifier to
use in your component or library:
DECLARE_UNIQUE_IDENTIFIER_TYPE(MyIdentifier);
DECLARE_UNIQUE_IDENTIFIER_TYPE(YourIdentifier);
MyIdentifier mine; // This is now an instance of my identifier type.
YourIdentifier yours; // This is an instance of your identifier type.
mine = yours; // ERROR: these types are not compatible!
Create a new identifier type for each kind of thing you want to identify. For example, the interaction library defines:
ElementIdentifier for UI elementsCustomElementEventType for unique custom UI eventsUntypedStateIdentifier for states that are tracked during testsTo create a unique identifier value, do one of the following:
DECLARE/DEFINE*UNIQUE_IDENTIFIER_VALUE macros.Using the existing macros:
// The identifier type needs to be declared somewhere.
DECLARE_UNIQUE_IDENTIFIER_TYPE(MyIdentifier);
// In a .h file where you want to create identifier values:
DECLARE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyIdentifierValue1);
DECLARE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyIdentifierValue2);
// In the corresponding .cc file:
DEFINE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyIdentifierValue1);
DEFINE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyIdentifierValue2);
Creating your own convenience macros:
// The identifier type is typically declared in the same file as the
// convenience macros.
DECLARE_UNIQUE_IDENTIFIER_TYPE(MyIdentifier);
#define DECLARE_MY_IDENTIFIER_VALUE(Name) \
DECLARE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, Name)
#define DEFINE_MY_IDENTIFIER_VALUE(Name) \
DEFINE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, Name)
// Then call your macros instead of the generic ones.
// In a .h file where you want to create identifier values:
DECLARE_MY_IDENTIFIER_VALUE(kMyIdentifierValue1);
DECLARE_MY_IDENTIFIER_VALUE(kMyIdentifierValue2);
// In the corresponding .cc file:
DEFINE_MY_IDENTIFIER_VALUE(kMyIdentifierValue1);
DEFINE_MY_IDENTIFIER_VALUE(kMyIdentifierValue2);
For an example of a full set of convenience macros, see ElementIdentifier.
The following macros/macro pairs are provided:
| Declaration (.h) | Definition (.cc) | Name | Usage |
|---|---|---|---|
| DECLARE_UNIQUE_IDENTIFIER_VALUE | DEFINE_UNIQUE_IDENTIFIER_VALUE | "Name" | For public values outside of a class |
| DECLARE_CLASS_UNIQUE_IDENTIFIER_VALUE | DEFINE_CLASS_UNIQUE_IDENTIFIER_VALUE | "Class::Name" | For values which are class members |
| DEFINE_LOCAL_UNIQUE_IDENTIFIER_VALUE | "File::Line::Name" | In an anonymous namespace or inside a function | |
| DEFINE_MACRO_LOCAL_UNIQUE_IDENTIFIER_VALUE | "File::Line::Name" | Use when calling from another macro |
When creating convenience macros, you must use
DEFINE_MACRO_LOCAL_UNIQUE_IDENTIFIER_VALUE() instead of
DEFINE_LOCAL_UNIQUE_IDENTIFIER_VALUE() or else the file and line numbers will
be wrong and the names may not be unique.
Examples:
// In .h file:
DECLARE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyPublicId);
class MyClass {
public:
DECLARE_CLASS_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyClassId);
void Func();
};
// In .cc file:
namespace {
DEFINE_LOCAL_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyFileLocalId);
}
DEFINE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyPublicId);
DEFINE_CLASS_UNIQUE_IDENTIFIER_VALUE(MyClass, MyIdentifier, kMyClassId);
void MyClass::Func() {
DEFINE_LOCAL_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyFunctionLocalId);
}
Variables of an identifier type start with a null value, which is always the default-constructed value of an identifier variable.
You should primarily assign identifiers from identifier values created using the declaration macros (see above).
You prefer to pass identifiers by value instead of const reference, since the size of an identifier and a reference are the same, and passing by value avoids having to dereference memory.
If you do need to communicate an identifier across process boundaries (such as
to/from a WebUI), use GetName() and FromName():
void MaybeHighlightElement(MyIdentifier id) {
mojoRemote.HighlightElement(id.GetName());
}
void OnIdentifierSelectedFromRemote(std::string id_name) {
MyIdentifier id = MyIdentifier::FromName(id_name);
NotifyIdentifierSelected(id);
}
Note that until you have called GetName() at least once for a particular
identifier value, it will not be retrievable via FromName(). This is because
the lookup is lazily created.
If you want to ensure that FromName() will work later in a class' lifespan,
you can force it to cache by calling GetName() in the constructor or during
initialization:
void Init() {
for (auto id : kKnownIdentifiers) {
id.GetName(); // Force-cache name for retrieval.
}
}
Because identifiers have predictable names, a WebUI can know the names of relevant IDs ahead of time.
DECLARE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kKnownIdentifier);
function report() {
proxy.identifierSelected('kKnownIdentifier');
}
Refer to the table above to see how identifiers are named. Note that LOCAL
identifiers cannot be used in this way as they do not have stable names.
You can use IfChange...ThenChange to ensure that names are kept consistent
between identifiers in different languages/parts of the codebase.
A typed identifier combines a compile-time type with a unique identifier. Typed identifiers are used to create ways to store and look up data that are guaranteed type-safe at compile time without having to add RTTI or dynamic casting. (The objects are stored by their underlying unique identifier.)
Unowned user data makes heavy use of typed identifiers, and is a good place to start if you want to understand how they can be effectively used.
In order to use typed identifiers, you must first create your own [untyped] unique identifier type. You can use the corresponding macros to declare specific typed identifiers:
| Declaration (.h) | Definition (.cc) | Name | Usage |
|---|---|---|---|
| DECLARE_TYPED_IDENTIFIER_VALUE | DEFINE_TYPED_IDENTIFIER_VALUE | "Name" | For public values outside of a class |
| DECLARE_CLASS_TYPED_IDENTIFIER_VALUE | DEFINE_CLASS_TYPED_IDENTIFIER_VALUE | "Class::Name" | For values which are class members |
| DEFINE_LOCAL_TYPED_IDENTIFIER_VALUE | "File::Line::Name" | In an anonymous namespace or inside a function | |
| DEFINE_MACRO_LOCAL_TYPED_IDENTIFIER_VALUE | "File::Line::Name" | Use when calling from another macro |
Again, you can create your own convenience macros, and again, you must call
DEFINE_MACRO_LOCAL_TYPED_IDENTIFIER_VALUE() instead of
DEFINE_LOCAL_TYPED_IDENTIFIER_VALUE() when calling from your own macros.
Example of using typed identifiers to store data. This approach is used in
various systems including UnownedUserDataHost and
[Un]OwnedTypedDataCollection.
// Class which stores literal values of various types.
class MyDataCollection {
public:
// Note that types can be defined inside of classes.
DEFINE_UNIQUE_IDENTIFIER_TYPE(MyUntypedIdentifier);
// Define this for convenience.
template<typename T>
using MyTypedIdentifier = ui::TypedIdentifier<MyUntypedIdentifier, T>;
// Can put limitations on what can be stored.
template<typename T>
requires !std::is_reference<T> && !std::is_pointer<T>
void StoreData(MyTypedIdentifier<T> typed_id, T value);
template<typename T>
T RetrieveData(MyTypedIdentifier<T> typed_id);
private:
std::map<MyUntypedIdentifier, struct DataStorage> storage_;
};
// Using the collection class we created:
DECLARE_TYPED_IDENTIFIER_VALUE(
MyDataCollection::MyUntypedIdentifier, int, kMyIntValue);
DECLARE_TYPED_IDENTIFIER_VALUE(
MyDataCollection::MyUntypedIdentifier, std::string, kMyStringValue);
MyDataCollection coll;
coll.StoreData(kMyIntValue, 3);
coll.StoreData(kMyStringValue, "foo");
const auto result = coll.RetrieveData(kMyIntValue);
CHECK_EQ(3, result);
Example of using typed identifiers to create objects; this approach is used in
e.g. the Kombucha ObserveState verb.
DEFINE_UNIQUE_IDENTIFIER_TYPE(MyUntypedIdentifier);
template<typename T>
using MyTypedIdentifier = ui::TypedIdentifier<MyUntypedIdentifier, T>;
// Construct a polymorphic object from arguments.
template<typename T, typename... Args>
requires std::derived_from<T, BaseType>
std::unique_ptr<BaseType> CreateInstance(MyTypedIdentifier<T> id,
Args&& args...) {
return std::make_unique<T>(std::forward<Args>(args)...);
}
Create a new identifier type for each conceptual type of thing you want to
identify. Previously, we used ElementIdentifier for everything and it was
both very confusing and easy to cross-contaminate or misuse an identifier for
the wrong thing.
Have a naming convention, especially for identifiers in the global scope. For example, in browser_element_identifiers all of the values for identifiers end with "ElementId". This avoids potential name collisions.
Use DECLARE/DEFINE_CLASS_ and DEFINE_LOCAL_ macros wherever possible,
with the latter being very useful for creating values in tests. This
completely avoids potential name collisions, as any number of local ids in
different files can have the same name without problems.
Identifiers are value types and should be passed by value when possible. Passing them by const reference is inefficient. You can pass an identifier variable by non-const reference or pointer if you intend for it to be an in/out parameter to the function.
void Func(MyIdentifier id); // Good.
bool Func(MyIdentifier& out_id, ...); // Fine.
void Func(const MyIdentifier& id); // Unnecessary (but not incorrect).
std::optional. // Good:
MyIdentifier GetIdentifier();
if (const auto my_id = GetIdentifier()) {
// my_id is guaranteed to be non-null.
}
// Unnecessary and maybe wrong unless you intended to have two null values:
std::optional<MyIdentifier> GetIdentifier();
if (const auto my_id = GetIdentifier(); my_id.has_value()) {
// my_id.value() may still be null/falsy!
}
Do not rely on sort order of specific identifiers (e.g. in std::set and
std::map) remaining stable across different invocations of the process.
Ordering is based on memory addresses of statically-allocated data, but
across different builds or even invocations this ordering might vary.
kMyIdentifier1 and kMyIdentifier2 as keys
in a map, but it's not safe to assume kMyIdentifier1 will always sort
before kMyIdentifier2 every time your program is run.Never use a DEFINE_... macro in a .h file.
All unique identifiers - typed or not - contain exactly one data member, which
is a const pointer to a UniqueIdentifierProvider. UniqueIdentifierProvider is a
struct which:
DEFINE*UNIQUE_IDENTIFIER_VALUE macros.constexpr).Therefore, the raw value of any identifier is the address of an impl struct, or null for invalid/default-constructed identifiers. This address is used to get the name corresponding to the identifier.
Names are cached in a global NoDestructor map lazily when they are retrieved,
allowing subsequent lookup by name. This avoids process load initialization. The
size of the map does not grow monotonically since identifiers can only be
created at compile time, so there are always O(1) possible entries.
Each macro also creates one constexpr identifier of the given type with the
specified name, which is linked to its corresponding impl. The name attached to
the impl corresponds to the name of the constant in a predictable way as per the
table above. (Note: exported identifiers are merely const.)
For example:
DEFINE_UNIQUE_IDENTIFIER_TYPE(MyIdentifier);
// Creates `constexpr MyIdentifier kMyId` in the current namespace:
DECLARE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyId);
// Implements a `const internal::UniqueIdentifierProvider` with name "kMyId".
DEFINE_UNIQUE_IDENTIFIER_VALUE(MyIdentifier, kMyId);
Because the UniqueIdentifierProvider associated with an identifier must have a
stable address process-wide, they cannot be defined in .h files, lest different
compilation units/modules generate different instances and use different
underlying memory address values.
This can, in turn, lead to cases where two values that should be the same but don't compare as equal, and - worst-case - to name collisions in the lookup that will crash the program.
There is no easy workaround for this limitation.
Currently, the names for identifiers of different types all go into the same lookup table. This means that if you have two different identifier types, and each declares an identifier called "kId", then the names will conflict.
This is almost certainly bad code, for reasons set out in the Best Practices section above. However, as usage of unique identifiers increases, collisions become more and more likely.
One option would be to prefix each name with the identifier's type (e.g. "ElementIdentifier:kBrowserElementId").
Another option would be to create a separate lookup for each identifier type, but that introduces a few additional issues like:
UniqueIdentifier values by name.Regardless of which approach is taken, it could potentially allow for run-time type-checking when retrieving an identifier by name.