c++reflectionstatic-reflectionc++26

What syntax is expected in C++26 for static reflection?


As far as I know, static reflection is currently on the roadmap for C++26.

The reflection TS proposes a type-based syntax, but a value-based syntax has also been proposed in the meantime. In P2560 Matúš Chochlı́k presented a comparison of the both approaches.

Has a decision already been made on which approach is likely to be standardized on?


Solution

  • The C++ committee met in Kona last week (November 2023), and this was discussed in SG7.

    The direction for Reflection for C++26 is outlined in P2996, which is a value-based reflection. At a high level:

    SG7 unanimously approved this design, there was no interest in discussing further pursuit of the Reflection TS (i.e. type-based reflection).

    In the Wrocław meeting (November 2024), the syntax was slightly adjusted per P3380. The original reflection operator was ^e and is now ^^e. This change was made due to ambiguity with Clang Blocks and was propagated through P2996 already.


    In terms of some of the P2560 commentary, I disagree with some of the points made - in particular the arguments that the "pros" of type-based is that it has "better usability", "is easier to teach", and is "more friendly to generic programming."

    I'll just note one of the examples in the reflection paper which is to implement make_integer_sequence. The goal here is that make_integer_sequence<int, 5> instantiates integer_sequence<int, 0, 1, 2, 3, 4>. How do you implement that in value-based reflection?

    template<typename T>
    consteval std::meta::info make_integer_seq_refl(T N) {
      std::vector<std::meta::info> args{^^T};
      for (T k = 0; k < N; ++k) {
        args.push_back(std::meta::reflect_value(k));
      }
      return substitute(^^std::integer_sequence, args);
    }
    
    template<typename T, T N>
      using make_integer_sequence = [:make_integer_seq_refl(N):];
    

    Here, substitute is a reflection API that takes a reflection of a template and a range of reflections of parameters and returns a reflection of the instantiation of that template. For instance, substitute(^std::tuple, {^int, ^char}) gives you ^std::tuple<int, char>, except that it lets you live in the value domain. It's true that you have to learn what that is, but other than that - this is a fairly straightforward algorithm: you make a vector, and you push stuff onto it. In this case, our "stuff" is heterogeneous, since we have one type template argument and a bunch of non-type template arguments, and that works fine since std::meta::info is the only type. How would you implement this in the type-based approach? The problem with type-based metaprogramming is that it cannot really be imperative - it must be functional.

    It's true that, as P2560 points out, splicing does require you to have a constant-expression to splice back. And that does certainly affect how you have to program things. But the availability of a rich API means that you can stay in the value domain longer than you might think, and that allows you to use the rest of the standard library API to get your work done. For instance:

    consteval auto struct_to_tuple_type(info type) -> info {
      return substitute(^^std::tuple,
                        nonstatic_data_members_of(type)
                        | std::ranges::transform(std::meta::type_of)
                        | std::ranges::transform(std::meta::remove_cvref)
                        | std::ranges::to<std::vector>());
    }
    

    Given something like struct S { int a; char& b; };, struct_to_tuple_type(^S) will give you a reflection of std::tuple<int, char>. This latter example is very easily doable in type-based reflection as well, it's just that instead of std::ranges::transform you'd use mp_transform from Boost.Mp11 and so forth. But it's a completely distinct sublanguage - which is why I question the pros listed in that paper?

    The paper also points out how count_if doesn't work. And it's true that this will be a case that is subtle that people have to learn about:

    template <typename T> inline constexpr bool my_trait = /* ... */;
    
    consteval auto num_const(span<info const> some_types) -> bool {
        // this one is built-in, so easy
        return std::ranges::count_if(some_types, std::meta::is_const_type);
    }
    
    consteval auto num_my_trait(span<info const> some_types) -> bool {
        // this one requires this one weird trick
        return std::ranges::count_if(some_types, [](std::meta::info type){
            // first, we need my_trait<T>, which is a substitute
            // then, we need to pull a value out of it, which we need to declare as bool
            return extract<bool>(substitute(^^my_trait, {type}));
        });
    }
    

    The paper is correct that this is inconsistent - though notably we can still use count_if for both, which I would argue makes it the approach that is more friendly to generic programming and easier to teach.

    And we can always come up with ways to make this better. Like:

    consteval auto trait_to_pred(std::meta::info trait) {
        return [=](auto... args){
            return extract<bool>(substitute(trait, {args...}));
        });
    }
    
    consteval auto num_my_trait(span<info const> some_types) -> bool {
        return std::ranges::count_if(some_types, trait_to_pred(^^my_trait));
    }
    

    I think, overall, that's less machinery than you would need for the type-based approach.