* What type of problems static reflection could solve, in general?
* Are there specific cases and / or situations where static reflection could resolve such case, even simplify an unnecessary complexity?
* What type of problems static reflection could solve, in general?
* Are there specific cases and / or situations where static reflection could resolve such case, even simplify an unnecessary complexity?
Does static reflection simplify such cases? ... Outlook unclear. It's definitely gnarlier to actually write the serialize() method, and in many cases, it does feel like a better option is to write a specific domain-specific language to specify what you want to specify, with a tool to operate on it as appropriate (think something like protobufs for serialization).
* Converting enum values to strings, and vice versa
* Parsing command line arguments from a struct definition (like Rust's clap)
* Simple definition of tuple and variant types, without the complex metaprogramming tricks currently used
* Automatic conversion between struct-of-arrays and array-of-structs form
* A "universal formatter" that can print any struct with all its fields
* Hashing a struct by iterating over its fields
* Convert between a struct and tuple, tuple concatenation, named tuples
Implementing serialization for complex types often requires manual code writing or external tools. With static reflection you could automate this process
template<typename T>
void serialize(const T& obj, std::ostream& os) {
for_each(reflect(T), [&](auto member) {
os << member.name() << ": " << member.get(obj) << "\n";
});
}
Simplified property systems class Person {
public:
Person(const std::string& name, int age)
: name(name), age(age) {}
std::string getName() const { return name; }
void setName(const std::string& name) { this->name = name; }
int getAge() const { return age; }
void setAge(int age) { this->age = age; }
private:
std::string name;
int age;
REFLECT_PROPERTIES(
(name, "Name of the person"),
(age, "Age of the person")
)
};
int main() {
Person person("Alice", 30);
auto properties = reflect::getProperties<Person>();
for (const auto& prop : properties) {
std::cout << "Property: " << prop.name
<< " (" << prop.description << ")" << std::endl;
auto value = reflect::get(person, prop.name);
std::cout << "Value: " << value << std::endl;
if (prop.name == "age") {
reflect::set(person, prop.name, 31);
}
}
std::cout << "Updated age: " << person.getAge() << std::endl;
return 0;
}
Simplified template metaprogramming template<typename T>
void printTypeInfo() {
constexpr auto info = reflect(T);
std::cout << "Type name: " << info.name() << "\n";
std::cout << "Member count: " << info.members().size() << "\n";
}
Easier to write generic algorithms that work with arbitrary types template<typename T>
void printAllMembers(const T& obj) {
for_each(reflect(T), [&](auto member) {
std::cout << member.name() << ": " << member.get(obj) << "\n";
});
}
enum class Color { Red, Green, Blue };
template<typename E>
std::string enum_to_string(E value) {
constexpr auto enum_info = reflect(E);
for (const auto& enumerator : enum_info.enumerators()) {
if (enumerator.value() == value) {
return std::string(enumerator.name());
}
}
return "Unknown";
}
template<typename E>
E string_to_enum(const std::string& str) {
constexpr auto enum_info = reflect(E);
for (const auto& enumerator : enum_info.enumerators()) {
if (enumerator.name() == str) {
return enumerator.value();
}
}
throw std::invalid_argument("Invalid enum string");
}
Parsing command line arguments from a struct definition struct CLIOptions {
std::string input_file;
int num_threads = 1;
bool verbose = false;
};
template<typename T>
T parse_cli_args(int argc, char* argv[]) {
T options;
constexpr auto struct_info = reflect(T);
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
for (const auto& member : struct_info.members()) {
if (arg == "--" + std::string(member.name())) {
if (member.type() == typeid(bool)) {
member.set(options, true);
} else if (i + 1 < argc) {
member.set(options, std::string(argv[++i]));
}
break;
}
}
}
return options;
}
Simple definition of tuple and variant types // Common data structure used in examples below
struct Person {
std::string name;
int age;
double height;
};
// Tuple, without reflection
int main() {
std::tuple<std::string, int, double> person_tuple{"John Doe", 30, 175.5};
std::cout << "Name: " << std::get<0>(person_tuple) << std::endl;
std::cout << "Age: " << std::get<1>(person_tuple) << std::endl;
std::cout << "Height: " << std::get<2>(person_tuple) << std::endl;
Person p{"Jane Doe", 25, 165.0};
auto p_tuple = std::make_tuple(p.name, p.age, p.height);
return 0;
}
// Tuple, with reflection
int main() {
std::tuple<std::string, int, double> person_tuple{"John Doe", 30, 175.5};
std::apply([](const auto&... args) {
(..., (std::cout << reflect(args).name() << ": " << args << std::endl));
}, person_tuple);
Person p{"Jane Doe", 25, 165.0};
auto p_tuple = std::apply([&p](auto... members) {
return std::make_tuple(members.get(p)...);
}, reflect(Person).members());
return 0;
}
// Variant, without reflection
int main() {
std::variant<int, std::string, Person> var;
var = 42;
std::cout << "Variant holds: " << std::get<int>(var) << std::endl;
var = "Hello, World!";
std::cout << "Variant holds: " << std::get<std::string>(var) << std::endl;
var = Person{"Alice", 28, 170.0};
const auto& p = std::get<Person>(var);
std::cout << "Variant holds Person: " << p.name << ", " << p.age << ", " << p.height << std::endl;
std::visit([](const auto& v) {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "Int: " << v << std::endl;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "String: " << v << std::endl;
else if constexpr (std::is_same_v<T, Person>)
std::cout << "Person: " << v.name << std::endl;
}, var);
return 0;
}
// Variant, with reflection
int main() {
std::variant<int, std::string, Person> var;
var = 42;
std::cout << "Variant holds: " << std::get<int>(var) << std::endl;
var = "Hello, World!";
std::cout << "Variant holds: " << std::get<std::string>(var) << std::endl;
var = Person{"Alice", 28, 170.0};
std::visit([](const auto& v) {
constexpr auto type_info = reflect(std::decay_t<decltype(v)>);
std::cout << "Variant holds " << type_info.name() << ": ";
if constexpr (type_info.is_class()) {
for (const auto& member : type_info.members()) {
std::cout << member.name() << ": " << member.get(v) << ", ";
}
} else {
std::cout << v;
}
std::cout << std::endl;
}, var);
return 0;
}
Automatic conversion between struct-of-arrays and array-of-structs template<typename Struct, size_t N>
auto soa_to_aos(const StructOfArrays<Struct, N>& soa) {
std::array<Struct, N> aos;
constexpr auto struct_info = reflect(Struct);
for (size_t i = 0; i < N; ++i) {
for (const auto& member : struct_info.members()) {
member.set(aos[i], soa.get(member.name())[i]);
}
}
return aos;
}
template<typename Struct, size_t N>
auto aos_to_soa(const std::array<Struct, N>& aos) {
StructOfArrays<Struct, N> soa;
constexpr auto struct_info = reflect(Struct);
for (size_t i = 0; i < N; ++i) {
for (const auto& member : struct_info.members()) {
soa.get(member.name())[i] = member.get(aos[i]);
}
}
return soa;
}
Universal formatter: template<typename T>
std::string format(const T& obj) {
std::ostringstream oss;
constexpr auto type_info = reflect(T);
oss << type_info.name() << " {\n";
for (const auto& member : type_info.members()) {
oss << " " << member.name() << ": " << member.get(obj) << ",\n";
}
oss << "}";
return oss.str();
}
Hashing a struct by iterating over its fields: template<typename T>
size_t hash_struct(const T& obj) {
size_t hash = 0;
constexpr auto type_info = reflect(T);
for (const auto& member : type_info.members()) {
hash ^= std::hash<decltype(member.get(obj))>{}(member.get(obj)) + 0x9e3779b9 + (hash << 6) + (hash >> 2);
}
return hash;
}
Convert between struct and tuple, tuple concatenation, named tuples: // Struct to tuple
template<typename Struct>
auto struct_to_tuple(const Struct& s) {
return std::apply([&](auto&&... members) {
return std::make_tuple(members.get(s)...);
}, reflect(Struct).members());
}
// Tuple to struct
template<typename Struct, typename Tuple>
Struct tuple_to_struct(const Tuple& t) {
Struct s;
std::apply([&](auto&&... members) {
((members.set(s, std::get<members.index()>(t))), ...);
}, reflect(Struct).members());
return s;
}
// Tuple concatenation
template<typename... Tuples>
auto tuple_concat(Tuples&&... tuples) {
return std::tuple_cat(std::forward<Tuples>(tuples)...);
}
// Named tuple
template<typename... Members>
struct NamedTuple {
REFLECT_NAMED_MEMBERS(Members...);
};
template<typename T>
std::string to_json(const T& obj) {
std::ostringstream oss;
constexpr auto type_info = reflect(T);
if constexpr (type_info.is_fundamental()) {
// Fundamental types (int, float, etc.)
if constexpr (std::is_same_v<T, bool>) {
oss << (obj ? "true" : "false");
} else if constexpr (std::is_arithmetic_v<T>) {
oss << obj;
} else if constexpr (std::is_same_v<T, std::string>) {
oss << "\"" << obj << "\"";
}
}
else if constexpr (type_info.is_enum()) {
// Enums
oss << "\"" << type_info.enum_name(obj) << "\"";
}
else if constexpr (type_info.is_array() || std::is_same_v<T, std::vector<typename T::value_type>>) {
// Arrays and vectors
oss << "[";
bool first = true;
for (const auto& elem : obj) {
if (!first) oss << ",";
oss << to_json(elem);
first = false;
}
oss << "]";
}
else if constexpr (std::is_same_v<T, std::map<typename T::key_type, typename T::mapped_type>>) {
// Maps
oss << "{";
bool first = true;
for (const auto& [key, value] : obj) {
if (!first) oss << ",";
oss << "\"" << key << "\":" << to_json(value);
first = false;
}
oss << "}";
}
else if constexpr (type_info.is_class()) {
// Classes and structs
oss << "{";
bool first = true;
for (const auto& member : type_info.members()) {
if (!first) oss << ",";
oss << "\"" << member.name() << "\":" << to_json(member.get(obj));
first = false;
}
oss << "}";
}
return oss.str();
}
enum class Color { Red, Green, Blue };
struct Address {
std::string street;
std::string city;
int zip;
};
struct Person {
std::string name;
int age;
double height;
Color favorite_color;
Address address;
std::vector<std::string> hobbies;
std::map<std::string, int> scores;
};
int main() {
Person person {
"John Doe",
30,
175.5,
Color::Blue,
{"123 Main St", "Anytown", 12345},
{"reading", "hiking", "coding"},
{{"math", 95}, {"history", 88}, {"science", 92}}
};
std::cout << to_json(person) << std::endl;
return 0;
}
I have a scripting layer in a game that needs to set properties in a C++ Model. I used a single Boost.Describe macro per struct and a generic get/set property. It worked very well and made me get rid of a lot of boilerplate.
https://www.boost.org/doc/libs/develop/libs/describe/doc/htm...
Addressing real problems with simple solutions isn't allowed.
This was a video game mod, essentially. I needed to create a text interface to modify settings for any other mod that might be installed. Other mods would simply implement a settings class with certain attributes, then I could list out all fields and their types. The list was processed into a sort of tree presented through the chat interface. From there I can generate code to modify that settings class from outside its assembly and raise value change events.
The reflection part of that was extremely simple, but just because that's how C# works. C# makes a task like this almost trivial.
At my current job, we have a similar thing. Classes decorated with attributes. We inspect them and check the generic type they implement. This way we register message handlers by their message type dynamically. You write a handler class and it simply works.
Windows Forms had a PropertyGrid control which did the same thing as my text interface, but with a grid of properties you can edit freely.
Most of this stuff is typically done at runtime. But you could have it be static if you wanted. A precious job did this to access the backing array inside of a List<> object. I offer no explanation or excuse for that one.
Because the library doesn’t need to allocate unless the underlying types being parsed to do, it has been constexpr since C++17 too.
A bunch of people have used libraries that use macros for reflection like or PFR to integrate other C++ JSON Libraries too.
Same for command line parameters. We want documentation strings, maybe dashes in the name etc.
But that can surely be solved with a little more advanced struct
I guess that's not what you wanted to say, but I fully agree :)
I suppose I just yearn for an all-in-one build system + package manager like exists in Rust or Go. Once you've seen what can be possible when these things are integrated from the ground up it sort of ruins your C++ build experience!
Serialization
#include <print>
template<typename T>
void serialize(const T& obj, std::ostream& os) {
for_each(reflect(T), [&](auto member) {
std::print("{}: {}\n", member.name(), member.get(obj));
});
}
Simplified property systems class Person {
public:
Person(const std::string& name, int age)
: name(name), age(age) {}
std::string getName() const { return name; }
void setName(const std::string& name) { this->name = name; }
int getAge() const { return age; }
void setAge(int age) { this->age = age; }
private:
std::string name;
int age;
REFLECT_PROPERTIES(
(name, "Name of the person"),
(age, "Age of the person")
)
};
int main() {
Person person("Alice", 30);
auto properties = reflect::getProperties<Person>();
for (const auto& prop : properties) {
std::print("Property: {} ({})\n", prop.name, prop.description);
auto value = reflect::get(person, prop.name);
std::print("Value: {}\n", value);
if (prop.name == "age") {
reflect::set(person, prop.name, 31);
}
}
std::print("Updated age: {}\n", person.getAge());
return 0;
}
Simplified template metaprogramming template<typename T>
void printTypeInfo() {
constexpr auto info = reflect(T);
std::print("Type name: {}\n", info.name());
std::print("Member count: {}\n", info.members().size());
}
Generic algorithm for printing all members template<typename T>
void printAllMembers(const T& obj) {
for_each(reflect(T), [&](auto member) {
std::print("{}: {}\n", member.name(), member.get(obj));
});
}
From there a lot of functionality for making statements about what something is has been bolted on to the side of what is really a very sophisticated type-aware preprocessor, and it shows. It's very painful to use it when you go from the core C++ language to the template, because the semantics are very different. Which the same can be said of C++->Preprocessor.
I think proper reflection should be a core part of the language that can be evaluated at compile-time with simple constructs. It's hard to articulate specifics but I think this proposal greatly simplifies working with type metadata to the degree that it's an approachable problem without using someone else's library. This proposal seems to do that in my opinion, and I think even some of the weaker programmers I've worked with could use this effectively.
>Reflection needs support from the compiler. Just add new syntax and put it on the compiler, ffs!
Converting enums to strings is one use case for reflection. Do you suggest introducing bespoke syntax for the other use cases too?
My gripe, if you will, is that converting an enum value to a string is a basic feature (as in, not reducible to other features) of every language that supports doing that. Not everything should be part compiler part library. And it doesn't need to be bespoke syntax. Enum values are already objects from the point of view of the compiler. Just give them a str member. This is similar to how in Rust built-in integers also have members. It's not bespoke, it's using already-existing syntax for objects and extrapolating it to other types. Another alternative that wouldn't involve bespoke syntax would be giving enum values an operator const char *() overload, either explicit or implicit.
>Converting enums to strings is one use case for reflection. Do you suggest introducing bespoke syntax for the other use cases too?
The other cases are pretty much all variations of enumerating members of a class. I have no problem with those examples, since it's basically how it's done everywhere. You get a meta object for the type that a function that enumerates the members of the type, and you go through it.
Because other than the free function vs member access thing, I don't see why it would concern the user of `enum_to_string()` that it's a proper function instead of a keyword like `static_assert`...
I am not disagreeing, just more saying what we have now.