←back to thread

196 points svlasov | 4 comments | | HN request time: 0.883s | source
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 #
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 #
fluoridation ◴[] No.40852303[source]
This is terrible. You can't just do Enum::Member.str or something?
replies(5): >>40852845 #>>40853083 #>>40853438 #>>40854151 #>>40855154 #
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 #
1. fluoridation ◴[] No.40857095[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 #
2. meindnoch ◴[] No.40858658[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 #
3. fluoridation ◴[] No.40859657[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 #
4. meindnoch ◴[] No.40860522{3}[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`...