c++c++26

What is `template for` in C++26?


I've seen a lot of examples for Reflection in C++26 that use a new kind of loop that I've never seen before like

template for (constexpr auto e : std::meta::enumerators_of(^^E)) {

What is template for? What's the point of this, how do I use it, and how does it differ from a regular range-based for loop?


Solution

  • template for is a new kind of loop called an expansion statement introduced in C++26 by P1306.

    Unlike all the other C++ loops, where the looping is based on some runtime condition, an expansion statement is a loop that happens entirely at compile time. Rather than running the same code N times, the compound-statement is stamped out N times during compile time. That is, each kind of expansion statement (which I'll get into shortly) evaluates as:

    S0;
    S1;
    ...
    Sn-1;
    

    For a synthesized statement Si.

    This allows the loop initializer to actually have a different type or a different constexpr value on each loop iteration. Because each instantiation is just a different statement.

    Expansion statements serve three basic purposes:

    1. They provide the ability to iterate over packs and tuples directly, which is otherwise only possible with awkward workarounds
    2. Expansion statements support early termination with break, unlike those awkward workarounds.
    3. They provide the ability to iterate over constants, preserving their constant nature.

    There are three kinds of expansion statements

    1. an enumerating expansion statement
    2. an iterating expansion statement
    3. a destructuring expansion

    The wording itself is quite easy to read, since we're effectively translating the code from an expansion statement into different code that it evaluates as. Would recommend reading that for more clarity.

    Enumerating Expansion Statement

    The simplest case, for when you want to iterate over a pack:

    template <class... Ts>
    void print_all(Ts... ts) {
      template for (auto t : {ts...}) {
        std::println("{}", t);
      }
    }
    

    Each Si is initializing the declaration with the ith expression in the pack and evaluating the statement.

    Iterating Expansion Statement

    The typical case for reflection uses. A lot of reflection functions return a std::vector<std::meta::info> that you need to iterate over, but you need each meta::info in that container to be usable as a constant expression for splicing purposes. The usual enum-to-string example looks like this:

    template <typename E>
      requires std::is_enum_v<E>
    constexpr std::string enum_to_string(E value) {
      template for (constexpr auto e : std::meta::enumerators_of(^^E)) {
        if (value == [:e:]) {
          return std::string(std::meta::identifier_of(e));
        }
      }
    
      return "<unnamed>";
    }
    

    We need e to be a constexpr variable in order for [:e:] to be a valid expression.

    The rules here match the rules for a regular range-based for statement (the same rules for begin-expr and end-expr as spelled out in [stmt.ranged], except that:

    Unlike a regular range-based for loop, e can be declared constexpr.

    Each Si is initializing the declaration with the next element of the range and then evaluating the statement. Except this is done statically (unlike a regular range-based for statement).

    That first requirement — that the range variable is declared static constexpr — actually makes the above example ill-formed because we still cannot have a constexpr variable that allocates memory, which a std::vector does (if it's non-empty).

    Another reflection example, which also demonstrates the workaround for non-transient allocation, would be printing all of the non-static data members:

    template <class T>
    void print_members(T const& v) {
      std::print("{{");
      bool first = true;
    
      constexpr auto ctx = std::meta::access_context::unchecked();
      template for (constexpr auto mem : define_static_array(
        nonstatic_data_members_of(^^T, ctx)))
      {
        if (not first) {
          std::print(", ");
        }
        first = false;
    
        std::print(".{}={}", identifier_of(mem), v.[:mem:]);
      }
    
      std::print("}}");
    }
    

    Given something like:

    struct Point { char x; int y; };
    

    the call print_members(Point{.x='e', .y=4}) would print {.x=e, .y=4}.

    Destructuring Expansion Statments

    The last case is equivalent to structured bindings. That is, iterating over a tuple:

    void f() {
      int x = 1;
      template for (auto p : std::tuple(x, &x)) {
        if constexpr (std::is_pointer_v<decltype(p)>) {
          std::println("{}", *p);
        }
      }
    }
    

    Each Si is initializing the declaration (auto p) with the next element from the structured bindings declaration and then evaluating the statement. Since p is a templated entity, we do the kind of check we see above, and evaluate *p — even though that is valid for only one of the types in our tuple.

    This is roughly equivalent to

    auto&& [...us] = std::tuple(x, &x); // initialization
    auto __f = [&](auto p){
      if constexpr (std::is_pointer_v<decltype(p)>) {
        std::println("{}", *p);
      }
    };
    (__f(us), ...);
    

    Except that this formulation doesn't support break or continue, while expansion statements do with the expected semantics (continue jumps to the beginning of the next iteration, break jumps to the end of the last iteration).