Gamasutra: The Art & Business of Making Gamesspacer
Sponsored Feature: Behind the Mirror - Adding Reflection to C++
arrowPress Releases
March 23, 2019
Games Press
View All     RSS







If you enjoy reading this site, you might also want to check out these UBM Tech sites:


 

Sponsored Feature: Behind the Mirror - Adding Reflection to C++


May 17, 2011 Article Start Previous Page 2 of 3 Next
 

To Parse or Not to Parse...

Using a parsing tool to analyze your code introduces a lot of complexity. C++ has a very complex syntax. While there are some tools you can take off the shelf to do the parsing, there is still a lot of work to do to make that data usable at runtime. Typically, you want to extract just the necessary data from the abstract parse tree and write out a meaningful representation of only the data that is required for what you want to reflect upon. Templates, typedefs, functions, and other language features are generally overkill for the purpose of reflecting upon fields in a class.

A parsing tool is probably going to do one of two things: write a data file to be loaded at runtime (or packed into the executable as a global variable or resource section), or generate some code that gets compiled into your program.

If you choose the data file route, you have the added task of computing member size and offset information. This information is compiler specific and target platform specific. By choosing this approach, you are committing to abide by the padding and alignment rules of whatever compiler you use to build any given version of your program. Another source of complexity comes from the existence of two independent pipelines processing information about your code: the compiler and the parsing tool. This necessitates synchronizing the data output from the tool with the specific version of the compiled program, which will make packaging and deploying your program harder. Synchronization is a very important problem to solve in this approach because not detecting out-of-sync reflection information can cause nasty bugs (and potentially mangled data).

If you choose to generate source code to be compiled into your program, you inherit the burden of the complexities that come with creating a code generator that is most likely specific to your particular needs. The code generation tool will probably need to make a bunch of decisions about how your code needs to be decorated and organized. These requirements will change as your codebase evolves, and it will require you to be diligent about releasing and configuring your own build tool. Also, maintaining a tool that governs the ability to compile your game is risky because it has a tendency to break at the worst possible time (during a milestone).

The reward for using these approaches is tangible. You don't have any code that needs to be written by hand to reflect upon your classes. If you choose to generate code, then you will also probably get great performance since you can generate function bodies that do specific operations on every field of your classes, just like you would have done if you weren't using reflection at all.

In reality, there are a ton of moving parts when using this approach. Things can break in hard-to-trace ways if any step of the pipeline doesn't work as expected. Having implemented and maintained this technique for many years, I can tell you that there are days when it feels like the planets have to align for all the parts in this complex pipeline to actually work together in harmony.

Hand Coding

Alternately, code can be written to populate the reflection data model when our program starts up. This code creates class information structures, populates them with information about every field within the class, and adds them to the registry. Writing this code sounds arduous, but C++ template support provides some excellent tools to accomplish this with remarkably concise and manageable code. A good goal for this is to extract as much information as possible in a single function call per field, per class (just like our visitor function). This allows us to avoid any time spent at build time processing source, managing dependencies on build tools, dependency checking generated code, and synchronizing externally loaded data.

Polymorphic Data

Because containers in C++ are template types instead of concrete types, function overloading can only take us so far. Since each template instantiation is a completely different type, trying to support containers using a visitor pattern could lead to a combinatorial explosion in the number of overridden functions. Enumerated data types present the same challenges. It's not easy to support them via overloading, since every enum in the entire game would need a different overload.

A solution to this shortcoming is to delegate the handling of any piece of data to a separate class of object that can interface with individual fields using a pointer. This will give us the ability to operate on any data in a polymorphic manner, including integer, floating point, and enumerated data types. Many languages that require derivation from a canonical Object class do this already. Adding support for treating simple types with polymorphism doesn't mean that it's necessary to use the polymorphic versions of these types everywhere in your code. They will only be used to abstract away the implementation details of dealing with serializing, comparing, and converting data to and from human-readable strings (which is very handy for generating property UIs).

Truly polymorphic data can solve many edge cases and provide extensibility for user types like enums and exotic containers. It can also support user data types that need custom processing during serialization. If these data classes store a value in addition to working through a pointer, they can be used to interface with fields and store standalone data. This allows for interoperability between versions of the program that have slightly different fields without discarding this "unknown" information. This is a major coup for game development tools that revise sets of properties frequently between releases. You can publish a test release with a very different set of properties and know that, if content creators check in some of those files, they probably won't break anything for folks still using the stable production tools (since the stable tools data is still there in the files).

Every field in the reflection information will specify a class of object that will handle the details of reading and writing the necessary data to a persistence interface or other objects of the same type. With this in mind, it's time to declare some data structures to store Class and Field information, as seen in Listing 2.


Article Start Previous Page 2 of 3 Next

Related Jobs

Pixel Pool
Pixel Pool — Portland, Oregon, United States
[03.22.19]

Software Developer (Unreal Engine 4, Blueprint, C++)
Crystal Dynamics
Crystal Dynamics — Redwood City, California, United States
[03.22.19]

Senior Tools Engineer
Sucker Punch Productions
Sucker Punch Productions — Bellevue, Washington, United States
[03.22.19]

Open World Content Designer
Phosphor Studios
Phosphor Studios — Chicago, Illinois, United States
[03.22.19]

Senior Gameplay Programmer





Loading Comments

loader image