[In this Intel-supported Gamasutra article, Geoff Evans explores implementing a reflection system in C++ and how the payoff ultimately outweighs the effort. I worked with Geoff at Insomniac Games where he applied a lot of these ideas. Reflections allow for an interesting level of interaction between gameplay code and tools (especially the serialization of objects from data files). In a data-driven design, using a classes-own structure to inform behaviors can be very powerful.
This article was originally printed as a Game Developer magazine article in the February 2011 issue. It's worth reading, so here it is on Gamasutra!
- Orion Granatir]
Reflection is a programming language feature that adds the ability for a program to utilize its own structure to inform its behavior. Reflection has its costs, but those are often outweighed by the ability to automate the serialization of objects into and out of a file, cloning, comparison, search indexing, and network replication, type conversion (copying base data between derived class instances), and user interface generation.
Of course, all these tasks can be accomplished without reflection capabilities, but you will likely pay higher costs having to write code that is very rote and prone to error. A good implementation of reflection can provide a platform on which each of these problems can be solved without glue code in every class that desires these features.
At the highest level, reflection can encompass many different features, such as runtime knowledge of class members (fields and methods), dynamic generation and adaptation of code, dynamic dispatch of procedure calls, and dynamic type creation.
However, for the purposes of this article, I will define C++ Reflection to mean "having access at runtime to information about the C++ classes in your program."
Before diving headlong into how to add reflection to C++, it's worth noting what type of information is already built-in. The C++ language specification provides minimal information about the classes compiled into a program. When enabled, C++ Run Time Type Information (RTTI) can provide only enough information to generate an id and name (the typeid operator), and handle identifying an instance's class given any type of compatible pointer (dynamic_cast<>).
For the purpose of game programming, RTTI is often disabled entirely. This is because its implementation is more costly than a system built on top of C++. Even if a program only makes a handful of RTTI queries, the toolchain is typically forced to generate, link, and allocate memory at runtime for information about every class in the application (that has a vtable). This significantly increases the amount of memory required to load your program, leaving less memory available for face-melting graphics, physics, and AI. It's better to implement your own RTTI-like system that only adds cost to the classes that need to utilize it. There are plenty of practical situations where vtables make sense without needing to do runtime-type checking.
Thus, the first step in implementation of a reflection system is typically a user implementation of RTTI features. This can be accomplished with only a couple of steps. Type information can be associated by a static member pointer (which also makes a good unique identifier for any given type within the program). In addition, some virtual functions allow querying an object's exact type, as well as test for base class types:
// Returns the type for this instance
virtual const Type* GetType() const;
// Deduces type membership for this instance
virtual bool HasType( const Type* type ) const;
GetType returns a pointer to the static type data, and HasType compares the provided type against its static type pointer as well as every base class' type pointer. This gives us all the information needed to reimplement dynamic_cast<>, but it only adds overhead to classes that are worth paying the added cost of type identification and type checking.
The simplest technique for implementing reflection is to take a purely programmatic approach. Virtual functions can be a mechanism for the traversal of all fields in a class. The visitor design pattern provides an abstraction for performing arbitrary operations on the fields as in Listing 1.
This is a textbook implementation of the visitor design pattern. Objects deliver the visitor to each one of its fields and the visitor gets an opportunity to transact with each field in series. It offers excellent encapsulation since the object does not know or care about any implementation details of what the visitor is trying to accomplish.
This technique does not require data from an external tool to do its job since it's implemented entirely in the code compiled into the program. It's simple to step through and debug, and extensible since many operations can be implemented as another class of Visitor.
With this approach, the development cost is small. A single line of code for each field in every class in your codebase is a fair price to pay to attain the benefits reflection can provide. However, there are some drawbacks with using a visitor function for reflecting upon your objects. There are a lot of virtual function calls happening to interact with each field in a class. This is a concern for performance-critical code, and on certain platforms. Also, this technique is best suited for operations that want to visit every single field of a class. There are many situations where this work is not required, and iterating over every field just to access a few is wasteful and time consuming (depending on the size of the object).
To really take reflection to the next level, it's necessary to be able to address specific fields and read and write data without iterating over every field in the class. A data model that represents the classes and fields specified in the code is needed to accomplish this. At runtime, your program can reflect upon this model to interface with objects and their field data.
This data model is owned by a central registry of type information. This singleton object owns all the type information in the program and can have support for finding type information by name. It's also a central point where a map of the entire inheritance hierarchy of classes can be built. The registry can be populated by employing a parser tool to analyze your source code, or by adopting a method similar to the visitor function approach to populate this data model at program startup.