6 ms·
Can I ask a naive question that consists of two parts and please don't flame me? lol * What type of problems static reflection could solve, in general? * A
by stefanos82 2y ago
Can I ask a naive question that consists of two parts and please don't flame me? lol
* 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?
- adamnemecek 2y agoSerialization comes to mind.
- quotemstr 2y ago> What type of problems static reflection could solve, in general? Imagine making a plain struct Point { float x; float y; }; and wanting to serialize it to JSON without further ceremony
- throwway120385 2y agoThis is the thing that's driving me away from C++ very quickly. A big part of our code base is code that handles this, and it either has to be in a DSL and constantly recompiled or we have to make a bunch of boilerplate. It's a huge problem for the language not to be able to do this.
- greenavocado 2y agoExample of serializing a C++ object to JSON with reflection: 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; }
- emmelaich 2y agoThat is cool. Would it be possibly implement something like Golangs %v and %T printf formatters?
- intelVISA 2y agoSerialization is largely a Solved Problem in modern C++ thanks to template metaprogramming. Wrangling a kludge DSL instead of Serialize<T> betrays poor lang knowledge...
- utensil4778 2y agoOne could argue that template metaprogramming is a kludge DSL of its own.
- throwway120385 2y agoIt absolutely is. The root problem is that the language itself doesn't permit you to make statements about your constructs without learning an entirely different set of tools. Whereas other languages have these tools as a core component. Templates seem like they were added to resolve a bunch of problems with the macro system. You can see that in the earliest use of templates to eliminate a lot of boilerplate around containers that in C you might have used the preprocessor to do. 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.
- germandiago 2y agoI suggest you to take a look at Boost.Describe. 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/html/describe.html https://www.boost.org/doc/libs/develop/libs/describe/doc/htm...
- beached_whale 2y agoBoost.PFR is really neat and doesn’t need manual mapping for aggregate types
- germandiago 2y agoWhat I needed is accessing thosr by name. So PFR won't do.
- beached_whale 2y agoShould be able to build that on if it isnt there. But at runtime the type would need to be a sum type like variant.
- throwway120385 2y agoYeah, that looks like it would do what I need.
- sankhao 2y agoThis is doable in c++20 https://github.com/stephenberry/glaze https://github.com/stephenberry/glaze
- jcranmer 2y agoThe main canonical use case for static reflection is serialization, where serialize<T>() can easily have a default case of calling serialize() on each of the fields. In the more general case, you basically want to have some library method that does something based on the structure of a struct or class, without having to define some sort of explicit, intrusive interface that said struct or class implementation has to provide. 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).
- petters 2y agoSerialisation often needs additional information not present in the struct definition: for enabling backwards-compatibility and default values. 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
- edflsafoiewq 2y agoHere are some examples from the linked paper * 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
- greenavocado 2y agoConverting enum values to strings, and vice versa 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...); };
- greenavocado 2y agoAny sort of reflection brings C++ one step closer to Python. 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"; }); }
- tsimionescu 2y agoNote that you should really be using std:print rather than std::cout if using modern C++.
- npoc 2y agoJust because it's newer doesn't make it better. There are good reasons for avoiding iostream
- deleted 2y ago[deleted]
- bdd8f1df777b 2y agoWhat I commonly need is JSON serialization/parsing directly with structs.
- beached_whale 2y agoThey exist, for instance https://github.com/beached/daw_json_link https://github.com/beached/daw_json_link , uses mappings of JSON <-> class members/construction. It handles a bunch of types out of the box and allows custom types to be mapped. Also, it integrates with some of the reflection like libraries out there now(Boost.Describe/Boost.PFR) and will be trivial to add C++26 static reflection when available. 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.
- bdd8f1df777b 2y agoWithout static reflection, we need to write a lot of boilerplate code, and those boilerplate is not guaranteed to stay in sync with the struct definition. That is why static reflection is sorely needed.
- beached_whale 2y agoEven static reflection isn’t a panacea and we have it now the libraries like PFR that give us a reference and name of each member for aggregates. But this is the same problem for anything else using those types too. For non-aggregates we need reflection or some manual library. I am not disagreeing, just more saying what we have now.
- utensil4778 2y agoMaybe this doesn't count as static, but I used to regularly use reflection in C# to generate code for interacting with foreign DLLs. 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.