Most active commenters
  • pjmlp(5)
  • greenavocado(4)
  • fluoridation(4)
  • beached_whale(4)
  • throwway120385(3)
  • tsimionescu(3)
  • meindnoch(3)

←back to thread

196 points svlasov | 60 comments | | HN request time: 1.093s | source | bottom
1. stefanos82 ◴[] No.40851614[source]
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?
replies(8): >>40851661 #>>40851675 #>>40851709 #>>40851795 #>>40851942 #>>40852552 #>>40853159 #>>40853615 #
2. adamnemecek ◴[] No.40851661[source]
Serialization comes to mind.
3. quotemstr ◴[] No.40851675[source]
> 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
replies(2): >>40851792 #>>40853917 #
4. jcranmer ◴[] No.40851709[source]
The 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).

replies(1): >>40854102 #
5. throwway120385 ◴[] No.40851792[source]
This 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.
replies(3): >>40852051 #>>40852750 #>>40853258 #
6. edflsafoiewq ◴[] No.40851795[source]
Here 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

replies(1): >>40852011 #
7. greenavocado ◴[] No.40851942[source]
Any 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";
        });
    }
replies(1): >>40854231 #
8. greenavocado ◴[] No.40852011[source]
Converting 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...);
    };
replies(1): >>40852303 #
9. greenavocado ◴[] No.40852051{3}[source]
Example 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;
    }
replies(1): >>40855621 #
10. fluoridation ◴[] No.40852303{3}[source]
This is terrible. You can't just do Enum::Member.str or something?
replies(5): >>40852845 #>>40853083 #>>40853438 #>>40854151 #>>40855154 #
11. ◴[] No.40852552[source]
12. intelVISA ◴[] No.40852750{3}[source]
Serialization is largely a Solved Problem in modern C++ thanks to template metaprogramming. Wrangling a kludge DSL instead of Serialize<T> betrays poor lang knowledge...
replies(1): >>40853507 #
13. kllrnohj ◴[] No.40852845{4}[source]
That'll inevitably be a utility function that exists, but C++ generally prefers broadly useful language primitives over single-case helpers
replies(1): >>40854827 #
14. j16sdiz ◴[] No.40853083{4}[source]
This is how C++ language usually works. Just some primitive for building libraries on. Expect some library change come later.
replies(1): >>40854204 #
15. bdd8f1df777b ◴[] No.40853159[source]
What I commonly need is JSON serialization/parsing directly with structs.
replies(1): >>40853788 #
16. germandiago ◴[] No.40853258{3}[source]
I 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/htm...

replies(2): >>40853792 #>>40857633 #
17. andersa ◴[] No.40853438{4}[source]
Of course not. We must involve 10 layers of templates that mere mortals cannot read and compilers cannot process in reasonable time so the academics at the committee will be happy.

Addressing real problems with simple solutions isn't allowed.

18. utensil4778 ◴[] No.40853507{4}[source]
One could argue that template metaprogramming is a kludge DSL of its own.
replies(1): >>40857801 #
19. utensil4778 ◴[] No.40853615[source]
Maybe 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.

replies(1): >>40855385 #
20. beached_whale ◴[] No.40853788[source]
They exist, for instance 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.

replies(1): >>40862169 #
21. beached_whale ◴[] No.40853792{4}[source]
Boost.PFR is really neat and doesn’t need manual mapping for aggregate types
replies(1): >>40855268 #
22. sankhao ◴[] No.40853917[source]
This is doable in c++20 https://github.com/stephenberry/glaze
replies(1): >>40854242 #
23. petters ◴[] No.40854102[source]
Serialisation 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

replies(1): >>40854291 #
24. Aardwolf ◴[] No.40854151{4}[source]
Ouch, I first thought this was the example of how to do these things _before_ reflection is introduced to C++

The for loop required to do enum to string really makes it

replies(1): >>40854286 #
25. coopierez ◴[] No.40854204{5}[source]
Thankfully it is so easy to quickly import libraries into C++...
replies(2): >>40854324 #>>40859679 #
26. tsimionescu ◴[] No.40854231[source]
Note that you should really be using std:print rather than std::cout if using modern C++.
replies(2): >>40854358 #>>40856006 #
27. _flux ◴[] No.40854242{3}[source]
That's pretty neat! What's the C++20 feature that enables this?
replies(1): >>40855636 #
28. einpoklum ◴[] No.40854286{5}[source]
You can use `.name()` and that also works fine. Remember std::string is a heap-based thing.
29. einpoklum ◴[] No.40854291{3}[source]
I have one word for you: attributes (which the compiler, and the reflector, know about).
replies(1): >>40863257 #
30. pjmlp ◴[] No.40854324{6}[source]
It actually is, for anyone using Conan or vcpkg.
replies(1): >>40855473 #
31. npoc ◴[] No.40854358{3}[source]
Just because it's newer doesn't make it better. There are good reasons for avoiding iostream
replies(2): >>40854750 #>>40854788 #
32. tsimionescu ◴[] No.40854750{4}[source]
That's my point - iostream is a really bad piece of code, and if you're anyway going to use modern C++, it's really recommended to stop using it.
replies(2): >>40860669 #>>40864401 #
33. spacechild1 ◴[] No.40854788{4}[source]
> There are good reasons for avoiding iostream

I guess that's not what you wanted to say, but I fully agree :)

34. DrBazza ◴[] No.40854827{5}[source]
Unfortunately. std::string `contains` arrived in C++23
replies(1): >>40856306 #
35. meindnoch ◴[] No.40855154{4}[source]
What are you talking about? At runtime, an enum value is just an integer. You need to look up the enum case that corresponds to your integer in order to convert it to a string. Which is precisely what the code above is doing.
replies(2): >>40855465 #>>40857095 #
36. germandiago ◴[] No.40855268{5}[source]
What I needed is accessing thosr by name. So PFR won't do.
replies(1): >>40861684 #
37. gpderetta ◴[] No.40855385[source]
C# also has the compiler available at run time, which is an extremely powerful feature, so the line between static and dynamic reflection is blurred.
replies(1): >>40864410 #
38. kamray23 ◴[] No.40855465{5}[source]
Yeah, C++ enums are just numbers that are really good at pretending to be types. And for that reason they're not actually objects that contain things like their name. And they probably shouldn't, in the vast majority of cases it would be an utter waste of space to store the names of enum members along with them. So you have compile-time reflection for those instead. And yeah, you could implement some kind of thing pretending to be a member but actually being a reflection thing but that's both horrifying and limited so C++ takes the reasonable approach of just adding general reflection in instead.
39. coopierez ◴[] No.40855473{7}[source]
So our team switched to vcpkg recently and, while it has improved certain parts of our dependency process, it has also made other parts more complex. Notably when something suddenly goes wrong it is far more complex to figure out what actually happened (Though to be fair a lot of these issues also come from combining vcpkg with cmake). This led to most of my team revolting against vcpkg and now it looks like we might go back to just vendoring our libraries again.

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!

replies(1): >>40857722 #
40. emmelaich ◴[] No.40855621{4}[source]
That is cool. Would it be possibly implement something like Golangs %v and %T printf formatters?
41. gpderetta ◴[] No.40855636{4}[source]
From a quick look, generalized constexpr evaluation, but in practice it relies on parsing non-portable decorated function names from things like source location. An ugly, slow, but effective hack.
42. greenavocado ◴[] No.40856006{3}[source]
Fair enough.

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));
        });
    }
43. kllrnohj ◴[] No.40856306{6}[source]
That's a stdlib utility, not a language feature :)
44. fluoridation ◴[] No.40857095{5}[source]
Sure, but reflection requires some support from the compiler either way. There's no reason why if you have an expression like x.str, where the compiler can see that x is an enum, that it can't rewrite it into a table lookup like __Enum_strings[x]. This would work even if x is an unknown run-time value. This is basically what any other language that supports converting enums to strings natively does. I understand that the C++ committee prefers to delegate work to the standard library, but in this case it's stupid. Reflection needs support from the compiler. Just add new syntax and put it on the compiler, ffs!
replies(1): >>40858658 #
45. throwway120385 ◴[] No.40857633{4}[source]
Yeah, that looks like it would do what I need.
46. pjmlp ◴[] No.40857722{8}[source]
Until one needs to step out of a pure Go or pure Rust experience, and then it is a quite interesting build.rs file, Makefiles or shell scripts.
47. throwway120385 ◴[] No.40857801{5}[source]
It 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.

48. meindnoch ◴[] No.40858658{6}[source]
So your gripe is that you have to write `enum_to_string(x)` instead of `x.str()`? And this is of such importance, that this needs to be included in the C++ language itself, as a special case of the dot operator. Correct?

>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?

replies(1): >>40859657 #
49. fluoridation ◴[] No.40859657{7}[source]
>So your gripe is that you have to write `enum_to_string(x)` instead of `x.str()`? And this is of such importance, that this needs to be included in the C++ language itself, as a special case of the dot operator. Correct?

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.

replies(1): >>40860522 #
50. fluoridation ◴[] No.40859679{6}[source]
This would be the standard library so it's not really an issue.
51. meindnoch ◴[] No.40860522{8}[source]
So your problem is that C++ prefers free functions instead of these pseudo-members?

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`...

52. npoc ◴[] No.40860669{5}[source]
Sorry, I got your comment completely backwards
53. beached_whale ◴[] No.40861684{6}[source]
Should be able to build that on if it isnt there. But at runtime the type would need to be a sum type like variant.
54. bdd8f1df777b ◴[] No.40862169{3}[source]
Without 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.
replies(1): >>40862991 #
55. beached_whale ◴[] No.40862991{4}[source]
Even 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.

56. petters ◴[] No.40863257{4}[source]
Yes you’re right. That has worked well enough in other languages
57. pjmlp ◴[] No.40864401{5}[source]
iostream is good enough for most jobs, unless one is writing high performace IO code battling for each ms.
replies(1): >>40866381 #
58. pjmlp ◴[] No.40864410{3}[source]
Kind of, not when using Native AOT, and Roslyn is never available, unless packaged alongside the application.
59. tsimionescu ◴[] No.40866381{6}[source]
Or unless one wants to write formatted output, or unless one wants to handle IO errors with RAII...
replies(1): >>40866441 #
60. pjmlp ◴[] No.40866441{7}[source]
Perfectly fine with existing operators and handle classes.

Happily using iostreams since Turbo C++ 1.0 for MS-DOS in 1993, and will keep doing so, unless chasing ms optimizations.