Most active commenters
  • fluoridation(4)
  • meindnoch(3)

←back to thread

196 points svlasov | 20 comments | | HN request time: 0.001s | source | bottom
Show context
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 #
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 #
1. 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 #
2. fluoridation ◴[] No.40852303[source]
This is terrible. You can't just do Enum::Member.str or something?
replies(5): >>40852845 #>>40853083 #>>40853438 #>>40854151 #>>40855154 #
3. kllrnohj ◴[] No.40852845[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 #
4. j16sdiz ◴[] No.40853083[source]
This is how C++ language usually works. Just some primitive for building libraries on. Expect some library change come later.
replies(1): >>40854204 #
5. andersa ◴[] No.40853438[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.

6. Aardwolf ◴[] No.40854151[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 #
7. coopierez ◴[] No.40854204{3}[source]
Thankfully it is so easy to quickly import libraries into C++...
replies(2): >>40854324 #>>40859679 #
8. einpoklum ◴[] No.40854286{3}[source]
You can use `.name()` and that also works fine. Remember std::string is a heap-based thing.
9. pjmlp ◴[] No.40854324{4}[source]
It actually is, for anyone using Conan or vcpkg.
replies(1): >>40855473 #
10. DrBazza ◴[] No.40854827{3}[source]
Unfortunately. std::string `contains` arrived in C++23
replies(1): >>40856306 #
11. meindnoch ◴[] No.40855154[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 #
12. kamray23 ◴[] No.40855465{3}[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.
13. coopierez ◴[] No.40855473{5}[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 #
14. kllrnohj ◴[] No.40856306{4}[source]
That's a stdlib utility, not a language feature :)
15. fluoridation ◴[] No.40857095{3}[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 #
16. pjmlp ◴[] No.40857722{6}[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.
17. meindnoch ◴[] No.40858658{4}[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 #
18. fluoridation ◴[] No.40859657{5}[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 #
19. fluoridation ◴[] No.40859679{4}[source]
This would be the standard library so it's not really an issue.
20. meindnoch ◴[] No.40860522{6}[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`...