Reflection in C++. We waited for it! Or did we?
Reflection in C++. We waited for it! Or did we?
What is reflection?
Before start talking about reflection in my beloved C++, we need to figure out what is reflection.
According to Wikipedia:
In computer science, reflective programming or reflection is the ability of a process to examine, introspect, and modify its own structure and behavior.
In simple words, reflection allows the program to ask itself: “what do I consist of.”
Talking a bit more practical reflection allows us to figure out what fields the object has, which functions and maybe modify itself.
Let’s have a look at an example of reflection in some language where it already exists. Bellow there is a simple reflection example in Kotlin, a language where reflection is widely used and exists for a long time:
package dev.spgc
import kotlin.reflect.KProperty1
import kotlin.reflect.full.companionObject
import kotlin.reflect.full.declaredMemberFunctions
// Defining a class with some fields and some member functions
class Car(
val amountOfWheels: Int,
var ownerName: String,
var amountOfCrashes: Int,
var cost: Double
){
fun crash(damage: Double){
amountOfCrashes++
cost -= damage
}
fun sellTo(newOwner: String){
ownerName = newOwner
}
// Kotlin's way to define static functions
companion object{
fun expensiveCar(ownerName: String) = Car(
4,
ownerName,
0,
1000000.0
)
}
}
fun main() {
// Getting collection of all fields
val carFields = Car::class.members.filterIsInstance<KProperty1<Car, *>>()
// Getting collection of all member functions
val carMemberFunctions = Car::class.declaredMemberFunctions
// Getting collection of all static functions
val carStaticFunctions = Car::class.companionObject?.declaredMemberFunctions
println("Class car has ${carFields.size} fields and ${carMemberFunctions.size} member functions")
println("Fields are")
// Each reflected field of class has a visibility property,
// name and type which can be easily accessed and coverted to string
carFields.forEach {
println(" ${it.visibility} ${it.name}: ${it.returnType}")
}
println("Member functions are")
// Each reflected member and static function of class
// has a visibility property, name, return type, parameters descriptions and
// a lot of other useful things which can be
// easily accessed and coverted to string
carMemberFunctions.forEach {
val parameter_string = it.parameters.joinToString { param -> "${param.name}: ${param.type}" }
println(" ${it.returnType} ${it.name} (${parameter_string})")
}
println("Static functions are")
carStaticFunctions?.forEach {
val parameter_string = it.parameters.joinToString { param -> "${param.name}: ${param.type}" }
println(" ${it.returnType} ${it.name} (${parameter_string})")
}
}
STDOUT
Class car has 4 fields and 2 member functions
Fields are
PUBLIC amountOfCrashes: kotlin.Int
PUBLIC amountOfWheels: kotlin.Int
PUBLIC cost: kotlin.Double
PUBLIC ownerName: kotlin.String
Member functions are
kotlin.Unit crash (null: com.jetRelay.Car, damage: kotlin.Double)
kotlin.Unit sellTo (null: com.jetRelay.Car, newOwner: kotlin.String)
Static functions are
com.jetRelay.Car expensiveCar (null: com.jetRelay.Car.Companion, ownerName: kotlin.String)
The example above is extremely simple but shows the idea of how we can use reflection.
Why do we need it?
The question about a need of reflection is arguable. Some languages like C or C++ (before the 26th standard) survived without the reflection. So what’s the purpose?
Assume we have the project with a huge number of domain structures:
src
|_ domain
|_ struct1.h
|_ struct1.c
|_ struct2.h
|_ struct2.c
...
|_ struct100500.h
|_ struct100500.c
...
Each of these structures should be serializable to JSON (for some reason we want it). How can we implement this in C?
- By hand! Each structure should provide a function for JSON serialization. Take into account that all functions in C should have unique names, and voila you have a thousand of boilerplate lines of code that do JSON serialization. Moreover, you can’t call them in a loop, because all functions have different names, so you need a global function, which will parse all possible types. One more place for boilerplate!
- Using XMacro pattern. It’s more convenient, but if you have ever worked with a huge macros code base in C, you know that debugging it is a nightmare. In addition, you also should have a function or macro with
__Genericto be able to unify all calls to serialization
In C++ we have a more bright perspective for this task:
- Create abstract class:
Serializable, derive all domain models from it and implement serialization. Still a lot of boilerplates, but at least you don’t have a one function which parses all available data types - Also use XMacro; for C++ it will be a complete nightmare, since C++ classes are much more complex than C structs. In addition, this approach can’t be used for parameterized classes, as templates will be opened only during compilation, not preprocessing – where macros are opened But even if you are OK with writing a lot of boilerplate, imagine the following situation: you’ve decided to move from JSON to YAML. In this case you need to rewrite all the functions you have.
But how can reflection help us with the problem of a lot of boilerplate in serialization (and to be honest, not only in serialization)?
One function to rule them all!
You implement a function which will take any object, go through all its fields, and store them in JSON/Yaml whatever you want. Deserialization works in the same way: you read JSON and construct the object accordingly. If you’ve decided to change the format, you need to change or add only one function, not 100500.
So, only parsing JSON?
And no, reflection is powerful tool. Using it we can create a lot of different utills:
- ORM
- Config serialization
- Customizable
to_stringfunctions - Dependencies injectors
- Test frameworks (move from macros to reflection)
- Plugin systems
- RPC and REST automatization
A lot of these utils are already implemented in C++ right now. However, after adding the reflection, a lot of tools can be rewritten with bare C++, without using external generators (like in Protobuf with protoc), custom preprocessors (like in QT) or macros (like in gTest).
So if you looked for the opportunity to start an open source project in C++ with almost empty field, it is a great time!
What will be in C++?
The reflection will be added in C++ with a new 26th standard along with a lot of other features extending compile-time. The standard is not finished yet. The current post is referencing P2996R13 proposal, which already has some partial implementations in a few forks of major compilers. So some syntax still can be changed, but I hope the core ideas will remain the same.
New operators
Reflection operator
Firstly in a new standard, a new operator will be added. The reflection operator will allow you to get the reflection representation of any type:
constexpr auto r = ^^int; // The reflection representation of int
This operator will be used as an entrypoint to a reflection world
**
**Splicer - reflection to code injection operator
One of the ways to leave the reflection world is to use [: :] operator:
[: ^^char :] char_var = '*'; // Same as char char_val = '*';
This operator takes reflection and uses it in code: using it as a type if reflection was a type, using it like a field of a class if reflection was a field of a class, etc. **
New type
A new type will be added: std::meta::info the single opaque type which will be returned by ^^ operator and will be used by the majority of the functions in meta header
New library
As mentioned above, a new library with a lot of simple, but still powerful functions will be added. The library is <meta>. I don’t think that explaining all the functions will be useful, you can see them in the proposal, so it’s better to show a few of them (IMHO the most useful) in examples and describe them there
Examples
Here there will be a list of examples compiled with Bloomberg fork of clang with commit hash 4c3e6ae840c5edf4ed972a255e9059d1a92d879a
Class fields printer
#include <iostream>
#include <meta>
#include <string>
#include <string_view>
template <typename T>
void print_fields()
{
constexpr std::meta::info class_info = ^^T;
std::cout << std::meta::identifier_of(class_info) << ":\n";
// This is one of the way to iterate over list of class data members.
// The proposed `template for` approach doesn't work for some reason
// with `display_string_of`
// DO NOT RELLY ON THIS. THIS IS INTERNAL THING OF CLANG FORK FROM BLOOMBERG
// AND MOST PROBABLY IT WILL BE CHANGED IN THE FUTURE
[:
std::meta::detail::pretty_printer<char>::expand
(
std::meta::nonstatic_data_members_of
(
class_info
, std::meta::access_context::unchecked()
)
)
:] >> [&]<auto field>()
{
constexpr std::string_view field_name = std::meta::identifier_of(field);
constexpr std::string_view field_info = std::meta::display_string_of
(
std::meta::type_of(field)
);
std::string access_modifier;
if constexpr (std::meta::is_public(field))
{
access_modifier = "public";
}
else if constexpr (std::meta::is_protected(field))
{
access_modifier = "protected";
}
else
{
access_modifier = "private";
}
std::cout << " "
<< access_modifier
<< " "
<< field_name
<< ": "
<< field_info
<< " "
<< std::endl;
};
}
struct Car
{
int cost;
int amountOfWheels;
Car()
: cost{0}
, amountOfWheels{}
, owner{} {}
private:
std::string owner;
};
int main(int argc, char *argv[])
{
print_fields<Car>();
}
STDOUT:
Car:
public cost: int
public amountOfWheels: int
private owner: basic_string<char, char_traits<char>, allocator<char>>
This is a classic example of reflection usage. Such functions can be extremely helpful in debugging and serialization
Here you can see a demo of a few functions and approaches:
std::meta::identifier_ofallows to get thestd::string_viewwith name of the reflected object (but for some reason it doesn’t work for reflections of primitive types)std::meta::display_string_ofallows to get thestd::string_viewwith name of the reflected object, in a proposal it’s said that this function should output human-readable and pretty output, however for now the only difference between this function andstd::meta::identifier_ofthat this function works with reflections of primitive types.std::meta::nonstatic_data_members_ofallows to getstd::vector<std::meta::info>of non-static fields of class basing on the context (which will be described a bit bellow)std::meta::is_public/protected/private()allows to know the access qualifier of the fieldstd::meta::type_ofallows to get type reflection of the reflected value
As you may notice to iterate over the members of the class I used pretty strange functionality:
IMPORTANT: IT WILL NOT BE THE PART OF STANDARD, BUT RATHER A TEMPORARY HACK IN THE BLOOMBERG FORK OF CLANG. DO NOT USE THIS IN YOUR CODE OR BE PREPARED FOR IT TO BE CHANGED IN THE FUTURE.
[:
std::meta::detail::pretty_printer<char>::expand
(
std::meta::nonstatic_data_members_of
(
class_info
, std::meta::access_context::unchecked()
)
)
:] >> [&]<auto field>()
{
// Some code
}
This happens because on the one hand the std::meta::nonstatic_data_members_of returns std::vector<std::meta::info>, which cannot be used in const-eval context which is required by std::meta::info, but on the other hand, the proposed template for construction for some reason doesn’t work correctly with display_string_of function (leads to crash).
So I had to use this hack, even though it’s a temporary solution and most probably will be changed in the future.
Speaking about contexts (as I promised above), the proposal provides the idea of contexts to access members of classes with different qualifiers. There are three contexts:
currentwhich inherits the current access rights (e.g. you have access rights like you call the class fields directly in the code)unprivileged- can see only public fieldsunchecked- can see everything
Unified debug function
#include <iostream>
#include <meta>
#include <string>
#include <string_view>
template <typename T>
void print_object(T t)
{
constexpr std::meta::info class_info = ^^T;
std::cout << std::meta::identifier_of(class_info) << ":" << std::endl;
template for
(
constexpr std::meta::info field : std::define_static_array
(
std::meta::nonstatic_data_members_of
(
class_info
, std::meta::access_context::unchecked()
)
)
)
{
constexpr std::string_view field_name = std::meta::identifier_of(field);
std::cout << " " << field_name << " = " << t.[: field :] << std::endl;
}
}
struct Car
{
int cost;
int amountOfWheels;
Car(std::string owner, int cost, int amountOfWheels)
: cost(cost)
, amountOfWheels(amountOfWheels)
, owner(owner) {}
private:
std::string owner;
};
int main(int argc, char *argv[])
{
Car car{"Bob", 50, 10};
print_object(car);
}
STDOUT:
Car:
cost = 50
amountOfWheels = 10
owner = Bob
This example is a continuation of the previous. And also can be useful in debugging
- As you can see the function can even access the private fields of the class, that’s all thanks to
Spliceroperator. - Also, there is correct iterating over the members of the class using
template forwhich generates const-eval context inside (simply speaking (which is not truly correct) thetemplate forstatement will copy-paste its body changing all depending on data types accordingly, this is a powerful thing not only in terms of reflection, but also in terms of unpackingstd::tuple, for example) - The
std::define_static_arraypromotes a compile-time array to static storage, which allow us to use containers likestd::vectorin the const-eval context (which is crucial for expansion statement liketemplate for)
Dictionary (map) serializer
#include <any>
#include <map>
#include <meta>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
template <typename T>
T dict_serializer(std::map<std::string, std::any> dict)
{
T result{};
// std::meta::access_context
constexpr auto ctx = std::meta::access_context::unchecked();
template for
(
constexpr std::meta::info member : std::define_static_array
(
std::meta::nonstatic_data_members_of(^^T, ctx)
)
)
{
constexpr std::string_view name = std::meta::identifier_of(member);
if (name.empty())
{
continue;
}
const auto it = dict.find(std::string{name});
if (it == dict.end())
{
continue;
}
using MemberType = typename[: type_of(member) :];
if (const auto p = std::any_cast<MemberType>(&it->second))
{
result.[: member :] = *p;
}
else
{
throw std::runtime_error
(
"dict_serializer: wrong type for field '"
+ std::string{name}
+ "'"
);
}
};
return result;
}
struct Car
{
int cost;
int amountOfWheels;
Car()
: cost{0}
, amountOfWheels{}
, owner{} {}
Car(std::string owner, int cost, int amountOfWheels)
: cost(cost)
, amountOfWheels(amountOfWheels)
, owner(owner) {}
private:
std::string owner;
};
int main(int argc, char *argv[])
{
std::map<std::string, std::any> dict =
{
std::pair("cost", std::any(50)),
std::pair("amountOfWheels", std::any(10)),
std::pair("owner", std::string("Bob")),
};
Car car = dict_serializer<Car>(dict);
// Function from the previous example
print_object(car);
}
STDOUT
Car:
cost = 50
amountOfWheels = 10
owner = Bob
Nothing new here, but it’s a great example, how unified JSON serialization/deserialization can be implemented.
New types generation
#include <string>
#include <meta>
#include <vector>
template <typename T>
struct struct_of_vec
{
struct impl;
consteval
{
// std::meta::access_context
const auto ctx = std::meta::access_context::unchecked();
// std::vector<std::meta::info>
const auto old_members = std::meta::nonstatic_data_members_of(^^T, ctx);
std::vector<std::meta::info> new_members = {};
// This is consteval context, so we are allowed to iterate over vector
for (const auto member : old_members)
{
const std::meta::info vec_type = std::meta::substitute
(
^^std::vector
, { std::meta::type_of(member), }
);
const std::meta::info mem_descr = std::meta::data_member_spec
(
vec_type
, {.name = std::meta::identifier_of(member)}
);
new_members.push_back(mem_descr);
}
std::meta::define_aggregate(^^impl, new_members);
};
};
template <typename T>
using struct_of_vec_t = struct_of_vec<T>::impl;
struct Car
{
int cost;
int amountOfWheels;
Car()
: cost{0}
, amountOfWheels{}
, owner{} {}
private:
std::string owner;
};
using Cars = struct_of_vec_t<Car>;
int main(int argc, char *argv[])
{
// Function from the frist example
print_fields<Cars>();
}
STDOUT
impl:
public cost: vector<int, allocator<int>>
public amountOfWheels: vector<int, allocator<int>>
public owner: vector<basic_string<char, char_traits<char>, allocator<char>>, allocator<basic_string<char, char_traits<char>, allocator<char>>>>
Here is an example of how reflection can be used for a generation of new types based on existing ones. Such approaches can help a lot in the fields, where third party code generators are used right now (like gRPC).
std::meta::substituteallows to substitute the template value of the templated object (class/function). Of course all of these in terms of reflectionstd::meta::data_member_specallows to generate data member description, which can be used bystd::meta::define_aggregatestd::meta::define_aggregateallows to populate incomplete class/struct/union with data members using their reflections (which can be constructed withstd::meta::data_member_spec)
Also, there is a one more important concept of C++26 - consteval block which block of code which will be executed in compile-time (something similar to static_assert((/*Some code*/,true))), that’s why we are allowed to interact with std::meta::info in a simple for loop.
Consideration about the static nature of reflection
Unlike reflection in most of the languages with side-runtime (like Python) the reflection in C++ is static and should be available in compile time.
It brings some limitations.
There are small inconveniences like: string representations are only std::string_view not std::string or iterating through fields can be tricky sometimes. But all of these are just unusual syntax that should be usual for C++ programmer 😉.
However, unusual syntax is the least important drawback of static reflection. With this approach you can’t make code generation based on config without recompilation and side scripts.
In addition, all the data required for the reflection should be available in the compile time. This means that the majority of the utils mentioned at the beginning of the post and examples should be created in a (single) header library manner.
Conclusion
So what’s next? Will the reflection improve our life? On the one hand, we haven’t received a silver bullet which can solve all the problems. But it looks like there is no such one! The proposed reflection, if it manages to get to the final version of the standard and to the compilers, definitely will make our lives easy. No more code generation scripts and tools everywhere, small and beautiful parsers and less usage of macros. In addition to this, C++ even with reflection will remain an extremely fast language all thanks to the static nature of the proposed reflection approach.
The main question right now is when to expect reflection in the compilers? To answer that first, it’s important to understand when the standard will be published. Usually the standard is published at the end of the year it references to. So in the case of the 26th standard, it will be at the end of 2026, although the draft is going to be finalized in March 2026. But compilers usually provide support for the features which are already in the draft (or in proposals). So I hope we can expect reflection in new stable versions of GCC and Clang after finalizing the draft. The current status of the reflection is:
- First, as mentioned above, “Bloomberg” has their fork of clang, which you can already use to play with the reflection. However, there is no information about merging it to the master branch of clang.
- The GCC already has implementation of most of the proposals of the 26th standard, however reflection and
define_static_{string,object,array}is not implemented yet, but looks like it will take little time to be finished!
So it’s a great time to touch the reflection and maybe start to create libraries even with the current unstable API, like simdjson team.
Links
- The proposal - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html
- Bloomberg compiler - https://github.com/bloomberg/clang-p2996
- GodBolt (compiler explorer):
- Template repository to play with reflection - https://github.com/SPGC/clang-p2996-environment