Reflection
Category | C++ |
---|
Goals
- Expose type information from C++ source code in editor builds.
- Instantiate objects from their type information.
- Modify data structures at run-time.
Usage
- Type introspection.
- Type factory.
- Serialization.
- Networking.
- Bindings to shader systems.
- Bindings to scripting systems.
- Dependency tracking.
- Copy and paste.
- Undo and redo.
- Collaboration.
RTTI
Run-time type information (RTTI) is a C++ feature that allows the type of an object to be determined at run-time.
RTTI is optional with some compilers, and must be enabled.
✔️
- Provides the
typeid
operator to to determine the type of an object at run-time.
- The type of an object is represented by the
std::type_info
class.
- The
std::type_info
class can be used to obtain the name of a type and compare types.
- Provides the
dynamic_cast
operator to safely cast an object using type information.
❌
- Available only for classes that are polymorphic (require at least a virtual destructor).
- Does not provide information about data members.
As RTTI lacks fundamental features that are required for a reflection system, it should be disabled and implemented with a custom solution.
Structure
There are two types of data: structured and unstructured.
Structured data works with the reflection system and is described as a graph of objects, themselves described as a set of properties.
Unstructured data is represented by a block of memory that cannot be introspected such as a texture resource.
A reflection system should fully describe structured data, but only provide unstructured data as an opaque type.
Strategies
Preprocessor Macros
A reflection system based on macros is generally implemented by injecting static members into types.
The system can be implemented using templates that are declared by the macros.
✔️
- The simplest solution.
❌
- Increases the code size. As the reflection system is generally only enabled in development builds, it's not a big issue.
- Requires macros to be defined for each reflected type and member.
- Duplication of declaration in C++ headers, and macros.
Examples
- CryEngine
Parsing and Code Generation
A more elegant approach is to modify the build system in two passes.
The first pass uses a custom utility to parse the C++ source code and generate additional files.
The second pass performs the compilation of the engine with the generated files.
There are two strategies to parse C++ code.
- Parse the actual C++ AST. It can be done with Clang for example, but it also requires additional metadata that is not part of the C++ AST. This metadata can be provided using C++ attributes, comments, or macros.
- Parse the C++ headers to detect macros. Some special empty macros can be used in the header files to decorate the types and members. These macros are generally empty, and only exist for the parser.
✔️
- Do not require a complex set of macros and templates.
❌
- Parsing C++ is more complex.
- Require changes in the build system to generate additional files.
Examples
- Unreal
Data Language and Code Generation
The previous solutions require the C++ source code to be decorated and are rather intrusive.
A different approach is to design a custom data language to describe the type and their members.
With a build system in two passes, the first pass parses the data specifications and generates additional C++ source files used by the second phase of the build system.
The C++ source files are left unchanged.
✔️
- Do not require a complex set of macros and templates.
- Do not require changes in the source code.
❌
- Implementing a custom language is more complex.
- Require changes in the build system to generate additional files.
Examples
- WinRT (IDL)