Lightweight, extensible descriptors for immutable type data.
The TypeRegistry pattern provides a uniform way to define, look up, and extend immutable descriptor records. It is used throughout promeki for types whose identity is a fixed set of read-only properties (primaries, transfer functions, memory operations, etc.) and where users may need to register additional entries at runtime.
A TypeRegistry class has four parts:
registerData(); the registry itself (and its populated entries) lives in the implementation file.registerType(), which starts at the UserDefined sentinel.registerData().const Data * pointer. All accessors are inline and compile away to direct struct member access. The wrapper is the only type that should appear in APIs and member variables; naked IDs exist only for constructing a wrapper.Constructing a wrapper from an ID performs a single registry lookup and caches the resulting const Data *. After construction, all access is a pointer dereference — no lookup, no indirection beyond the pointer itself.
Copying a wrapper copies one pointer. Comparing two wrappers compares two pointers. Both are trivially cheap.
Users extend the registry in two steps:
registerType() to allocate a unique ID. This uses a lock-free atomic counter, making it thread-safe and suitable for use in static initializers.registerData() with a populated Data struct whose id field is set to the new ID. After this call, constructing a wrapper from that ID resolves to the registered data.The wrapper is the API type. Function parameters, return values, and member variables should use the wrapper class, not the ID enum. Pass wrappers by const reference:
Because every wrapper has an implicit constructor from its ID enum, changing a parameter from ID to const Wrapper & is source-compatible: callers that pass an enum constant like PixelFormat::RGBA8_sRGB compile unchanged.
TypeRegistry ID enums are unscoped, so the compiler treats them as integers in overload resolution. When a class has another constructor whose first parameter is an integer-compatible type (e.g. uint8_t), the ID may silently match the wrong overload.
In this situation, provide an explicit ID overload as a disambiguation guard — a constructor that accepts the ID type directly and forwards to the wrapper overload:
This is the only case where an ID should appear in a public parameter list. Document the overload with a comment explaining why it exists so it is not removed during future cleanup.
| Class | Data Struct | Description |
|---|---|---|
ColorModel | ColorModel::Data | Color model / color space descriptors |
MemSpace | MemSpace::Ops | Memory space operation tables |
PixelMemLayout | PixelMemLayout::Data | Pixel memory layout (components, bit depths, planes) |
PixelFormat | PixelFormat::Data | Full pixel description (format + color model + ranges) |
See also: ColorModel, MemSpace, PixelMemLayout, PixelFormat.