* 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?
* 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
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...);
};
>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`...