What are Reflection and Introspection in C++ and Why Do You Need Them?
\n\nImagine you are writing a complex game engine or a data processing framework. At some point, you need to understand: "What kind of object is this? What methods does it have? Does it have a name field?"
In languages like Python or Java, this is done in one line — they have full reflection (the ability of a program to examine and modify its own structure at runtime). In C++, however, things are more complicated. C++ is a strongly statically typed language, and there is almost no reflection "out of the box." But there is introspection — a limited ability to obtain information about types at runtime.
\n\nIn this article, we will cover:
\n- \n
- How reflection differs from introspection; \n
- How RTTI (Run-Time Type Information) works; \n
- How to use
typeidanddynamic_cast; \n - How to emulate reflection using templates and macros; \n
- Modern alternatives (C++20/23) and libraries. \n
Ready to look into the "looking glass" of C++? Let's go!
\n\nIntrospection vs Reflection: What's the Difference?
\n\nBefore writing code, let's dot the i's.
\n\n- \n
- Introspection is the ability of a program to "look at itself" and obtain information about types, but not modify the program's structure. For example: "Find out the class name of an object." This exists in C++ via RTTI. \n
- Reflection is introspection + the ability to dynamically create objects, call methods, and modify fields. This is not in the C++ standard (yet). \n
In C++, we use the term "reflection" more often as a goal, and "introspection" as an available tool. But don't be afraid: even with limited tools, you can do cool things.
\n\nRTTI (Run-Time Type Information) — The Foundation of Introspection in C++
\n\nRTTI is a mechanism that allows you to determine the type of an object at runtime. It is enabled by default (although in some embedded systems it is disabled with the -fno-rtti flag).
The main players of RTTI:
\n- \n
typeid— an operator that returnsstd::type_info; \n dynamic_cast— safe casting of polymorphic types. \n
Example 1: Using typeid
\n\n#include <iostream>\n#include <typeinfo>\n\nclass Base { virtual void foo() {} };\nclass Derived : public Base {};\n\nint main() {\n Base* b = new Derived();\n std::cout << "Object type: " << typeid(*b).name() << std::endl;\n delete b;\n return 0;\n}\n\nOutput (depends on compiler): class Derived or 7Derived (mangled name).
Note: typeid only works with polymorphic classes (having at least one virtual method). If you remove virtual, you will get the type of the pointer, not the actual object.
Example 2: dynamic_cast — Safe Casting
\n\n#include <iostream>\n\nclass Animal { public: virtual ~Animal() = default; };\nclass Dog : public Animal { public: void bark() { std::cout << "Woof!\\n"; } };\nclass Cat : public Animal { public: void meow() { std::cout << "Meow!\\n"; } };\n\nvoid makeSound(Animal* a) {\n if (Dog* dog = dynamic_cast<Dog*>(a)) {\n dog->bark();\n } else if (Cat* cat = dynamic_cast<Cat*>(a)) {\n cat->meow();\n } else {\n std::cout << "Unknown animal\\n";\n }\n}\n\nint main() {\n Animal* pets[] = { new Dog(), new Cat(), new Animal() };\n for (auto p : pets) makeSound(p);\n // Don't forget to delete!\n for (auto p : pets) delete p;\n return 0;\n}\n\nThis is a classic example of introspection: we don't know exactly which object came in, but we can