This project is a Type Reflection system implemented based on the C++17 standard.
At compile time, the design blueprints (Types, Member Variables, Member Functions) of objects are determined.
At runtime (when the program starts), instances of these design blueprints (TypeInfo, PropertyInfo, MethodInfo) are generated.
It provides functionality to access member variables (Properties) and member functions (Methods) existing within object instances at runtime.
The main goals are to minimize the use of dynamic_cast during runtime casting and to implement functionality for dynamically accessing instance information via string names.
- Type Information (
TypeInfo): Stores and manages class names, hash codes, and parent class (SuperType) information. - Property Information (
PropertyInfo): Stores names, types, and memory offsets of member variables. Provides a type-safeGet/Setinterface. Safely supports bothconstandnon-constobjects.- (Note: Handling for
constmember variables is currently not applied.)
- (Note: Handling for
- Container Support (
ContainerPropertyInfo): Enables traversal and manipulation of custom containers likewtr::DynamicArray,wtr::StaticArray,wtr::HashSet, andwtr::HashMapvia a single interface. Provides a Zero-Allocation iterator wrapper. - Method Information (
MethodInfo): Stores names and signature information (return type, argument types) for member functions and static functions. Provides a type-safeInvokeinterface. - Safe Casting (
Cast): UtilizesTypeInfoto provide safe up/downcasting at runtime while maximizing the avoidance ofdynamic_cast. - Auto Registration: Uses
GENERATE,PROPERTY, andMETHODmacros to automatically register reflection information within the class definition. - C++17 Utilization: Actively uses
if constexpr,inline staticvariables, and template metaprogramming to minimize performance overhead and ensure code clarity.
The project has the following directory structure:
├── CMakeLists.txt # Main CMake build script
├── include/ # Public header files
│ ├── Type/ # Type-related headers (TypeInfo, TypeManager, TypeCast, TypeMacro)
│ ├── Property/ # Property-related headers (PropertyInfo, ContainerPropertyInfo, PropertyMacro)
│ ├── Method/ # Method-related headers (MethodInfo, MethodCall, MethodMacro)
│ ├── Utils.h # Shared template utilities
│ └── Reflection.h # Main header for users to include
├── src/ # Source files (Implementation)
│ ├── Type/ # Type implementation (.cpp)
│ ├── Property/ # Property implementation (.cpp)
│ └── Method/ # Method implementation (.cpp)
├── demofile/ # Example project using the library
├── externaldemofile/ # Example project using the library (via CMake External Project)
└── cmake/ # CMake helper scripts (Build options, Installation, etc.)include/: Location of all public header files, organized intoType,Property, andMethodsubdirectories by function.src: Location of implementation (.cpp) files for classes and functions defined in headers, mirroring theincludestructure.demofile,externaldemofile: Example codes demonstrating the actual usage of the library.cmake: Includes helper scripts for the CMake build system.
-
Include Header: Include
#include "Reflection/Reflection.h"to use all necessary features. -
Class Registration (
GENERATE): Add theGENERATE(ClassName)macro inside the class definition (in eitherpublicorprivatesection) that you want to reflect.#include "Reflection/Reflection.h" class IObject { GENERATE(IObject); public : virtual ~Object() = default; }; class ObjectA : public IObject { GENERATE(ObjectA); public : virtual ~ObjectA() = default; };
-
Property Registration (
PROPERTY): Add thePROPERTY(VariableName)macro immediately above the member variable declaration you want to access via reflection. Container types are automatically detected and registered.class ObjectB : public ObjectA { GENERATE(ObjectB); public : virtual ~ObjectB() = default; public : PROPERTY(m_integerValue); int m_intergetValue = 0; // Basic type PROPERTY(m_floatValue); float m_floatValue = 1.0f; PROPERTY(m_array); wtr::DynamicArray<int> m_array; // Automatic container support };
-
Method Registration (
METHOD): Add theMETHOD(FunctionName)macro immediately above the member function or static function declaration you want to invoke via reflection. (Function overloading is currently difficult to support with this macro approach)class ObjectC : public ObjectB { GENERATE(ObjectC); public : virtual ~ObjectC() = default; METHOD(Print); // Member function registration void Print(const std::string& message) { std::cout << message << std::endl; } METHOD(Add); // Const member function registration int Add(int a, int b) const { return a + b; } METHOD(Any); // Static function registration static void Any() { /*...*/ } };
-
API Usage (Iteration Example):
void IterateContainer(IObject* obj) { const Reflection::TypeInfo* typeInfo = obj->GetTypeInfo(); const Reflection::PropertyInfo* prop = typeInfo->GetProperty("m_array"); // Check if it is a container property if (auto* arrayProp = Reflection::Cast<const Reflection::ArrayPropertyInfo*>(prop)) { // Create iterator (No heap allocation) auto it = arrayProp->begin(obj); auto end = arrayProp->end(obj); for (; it != end; ++it) { int value = *static_cast<const int*>(it.get()); std::cout << value << std::endl; } } }
During the development of this reflection system, several significant technical challenges were encountered and resolved as follows:
-
Need for C++17 Standard:
- Problem: The initial design planned to define and initialize
staticmember variables in header files (for auto-registration ofProperty/Methodinfo). However, prior to C++17, initializingstaticmember variables in headers without theinlinekeyword caused linkage errors. - Solution: The project standard was upgraded to C++17 to utilize the
inline staticmember variable feature supported from C++17 onwards. This allowed for clean handling of reflection information definition and initialization within header files using only macros.
- Problem: The initial design planned to define and initialize
-
Learning and Applying Template Metaprogramming (TMP):
- Problem: Complex requirements involving compile-time type information manipulation, such as deducing parent class types (
SuperType), distinguishingconstmember functions, and analyzing function signatures. - Solution:
- SFINAE (Substitution Failure Is Not An Error): Learned and applied techniques using
Utils::TypeDetector(specifically utilizingUtils::TypeWrapper) to branch template implementations based on the existence of specific types (e.g.,SuperTypeorThisType). It was crucial to understand that SFINAE considers substitution failures only within the immediate context. if constexpr: Actively utilized C++17'sif constexprto branch code at compile-time based on traits of template argumentsTandU(e.g.,Utils::IsPointer,Utils::IsSame). This optimized performance by eliminating unnecessary runtime branches or virtual function calls.- Template Specialization: Understood the constraint that function templates cannot be partially specialized, and implemented
MethodCreatorby leveraging the fact that class templates support partial specialization. - Template Parameter Rules: Required understanding and application of subtle C++ template rules, such as the order of template parameters (Type -> Type Pack -> Non-Type) and limitations of non-type parameters (function pointer values are allowed, but using them for class template specialization has specific constraints).
- SFINAE (Substitution Failure Is Not An Error): Learned and applied techniques using
- Problem: Complex requirements involving compile-time type information manipulation, such as deducing parent class types (
-
Container Reflection and Iterator Abstraction (Container Support):
- Problem: Needed to control containers with different memory layouts, such as
wtr::DynamicArrayandwtr::HashMap, via a single interface (Iterator). A typical virtual function-based iterator (IIterator*) causes heap allocation (new) every time, leading to performance degradation and memory fragmentation. - Solution:
- Type Erasure & SBO: Implemented an
Iteratorclass with a fixed-size 64-byte buffer (alignas(void*) uint8_t m_storage[64]) to eliminate heap allocation. - Placement New & VTable: Implemented manipulation of the actual internal STL iterator within the buffer via lambda functions (
m_nextFunc,m_copyFunc). - Safety Assurance: Implemented
CopyFuncandDestroyFuncto perform Deep Copy during iterator copy/assignment and to explicitly call destructors, thereby perfectly preventing crashes (Double Free, List Corruption) in Debug Mode. - Auto Detection: Used SFINAE (
HasIterator,HasKey, etc.) inPropertyCreatorto automatically determine the container type (Array/Set/Map) at compile-time and generate the appropriateContainerPropertyInfo.
- Type Erasure & SBO: Implemented an
- Problem: Needed to control containers with different memory layouts, such as
-
Property
constCorrectness:- Problem: If a
Getfunction receives aconstobject but returns anon-constpointer, it violates C++construles and allows for dangerous code. Merging into a single function was impossible due to differing return types required by C++construles. - Solution: Overloaded the
Getfunction into a version acceptingconst U&(returning a const pointer) and a version acceptingU&(returning a non-const pointer). The actual implementation logic was written in a single privateGetImplfunction (const version), and thenon-const Getcalls thisImpland then safely appliesconst_castonly to the return value to avoid code duplication. This is a standard C++ idiom for handlingconst.
- Problem: If a
-
Type Safety and Simplification in Property
Set:- Problem: Initially,
GetandSetused the same type-checking logic (allowing inheritance relationships). However, forSet, assigning aninstancecaused object slicing, and assigning pointers caused issues with unintended polymorphism allowance. Specifically,instanceassignment posed a risk of memory corruption. - Solution: For API clarity and safety, the rule for
Setwas simplified to allow only strictly identical types (Utils::IsSame). This made API usage more predictable and fundamentally blocked all hazardous situations.
- Problem: Initially,
-
Static Generation of Method Objects (Avoiding Dynamic Allocation):
- Problem: Dynamically allocating
MethodCallobjects for each method usingnewcould cause memory fragmentation and management overhead. Attempting to usestaticvariables faced the issue where different functions with the same signature (e.g.,Foo(),Bar()) would share the samestaticobject. - Solution: Introduced a
MethodCreator<typename Func, Func func>template struct using non-type template parameters. Since this struct is instantiated uniquely for each function pointer value (func), declaring astatic MethodCallobject inside the struct allows for the creation of uniquestaticobjects for each function pointer withoutnew.
- Problem: Dynamically allocating
-
Header Circular Dependency:
- Problem: Header files such as
MethodInfo,MethodCall,Macro, andTypeInforeferenced each other, causing circular inclusion issues. - Solution: Split the initial
Macro.hinto functional units (TypeMacro.h,PropertyMacro.h,MethodMacro.h). In each header file, instead of#include-ing required classes, forward declarations were utilized as much as possible. The headers were included only in the.cppfiles where actual implementation was needed, breaking the circular dependency chain.
- Problem: Header files such as
-
Miscellaneous: Fixed compilation errors such as resolving
constcorrectness inCastfunction calls toGetTypeInfo()(changedGetTypeInfoinGENERATEmacro toconst), and fixing missingtypenamekeywords when using theGENERATEmacro inside template classes.