Thoughts on P1306 “Expansion Statements”

Daveed Vandevoorde’s P1306R1 “Expansion Statements” proposes three interesting extensions to the language: for... (auto arg : pack) and for... (constexpr int i : array) and for... (auto arg : tuple). I was in EWG in Kona when this paper was presented, and observed that the proposed syntax is slightly messed up by the way it conflates two of these three distinct cases. But upon reflection, I’m not sure it’s possible to solve the third case without some conflation anyway.

Here are three examples of functions (or function templates) using P1306R1’s proposed syntax.

Example 1. Pack-expansion without constexpr.

template<class... Ts>
void f1(Ts&&... args) {
    for... (Ts rt : args) {
        std::cout << rt << std::endl;
    }
}

The above snippet accepts a pack args and pack-expands it using P1306’s for... construct. So it would be almost exactly equivalent to this working C++17 code:

template<class... Ts>
void f1a(Ts&&... args) {
    ([rt = Ts(args)]() {
        std::cout << rt << std::endl;
    }() , ...);
}

Notice that if we’re expanding a pack of args, we must have received that pack from a function parameter list, and therefore the args... cannot possibly be constant expressions. Function parameters are never constant expressions.

Example 2. Constexpr without pack-expansion.

template<int N>
constexpr std::array<int, N> make_iota() {
    std::array<int, N> result{};
    for (int i=0; i < N; ++i) result[i] = i;
    return result;
}

template<class Tuple>
void f2(const Tuple& tuple) {
    constexpr auto arr = make_iota<std::tuple_size_v<Tuple>>();
    for... (constexpr int CT : arr) {
        std::cout << std::get<CT>(tuple) << std::endl;
    }
}

The above snippet creates a constexpr array of indices and then uses them as arguments to std::get. So it would be essentially equivalent to this C++17 code:

template<class Tuple>
void f2a(const Tuple& tuple) {
    std::cout << std::get<0>(tuple) << std::endl;
    std::cout << std::get<1>(tuple) << std::endl;
    std::cout << std::get<2>(tuple) << std::endl;
    [...]
}

except that it would magically know how many indices to unroll for. Notice that in this case there is no pack-expansion taking place within f2, nor even within iota. This snippet contains no variadic templates and no parameter-packs at all. Therefore, it seems awkward and “wrong” that it contains the pack-expanding ellipsis ... syntax.

Example 3. Constexpr and pack-expansion together in the same example.

At the moment there is no way to get a heterogeneous pack of arguments that is also usable in constant expressions, because packs can appear only as parameter packs, and function parameters are never usable in constant expressions.

But you might reasonably ask, “How do I iterate over the elements of a constexpr tuple and do something with each of them?” Tuples don’t have begin() and end(), so the usual ranged-for loop won’t work for them. But they also aren’t packs, so the kind of for... we used in Example 2 won’t work for them.

You can turn a tuple back into a parameter-pack using std::apply:

constexpr std::tuple<int, float, bool> tuple {42, 5.0f, true};

auto f = [](auto... args) {
    // etc.
};
std::apply(f, tuple);

But as soon as f receives args, you lose the constexpr-ness of args! So we’re back to Example 1.

If we want to iterate over a constexpr tuple, it seems that we must make up a special case. P1306R1 uses the same for... grammar for Example 3 as it uses for Examples 1 and 2. We explained how removing the ... from Example 2 seems like a good idea; but even if we come up with nice natural syntax for Examples 1 and 2, we still have to shoehorn Example 3 in somehow.

void f3() {
    constexpr std::tuple<int, float, unsigned long> tuple {42, true, 15uL};
    for... (constexpr auto CT : tuple) {
        std::integral_constant<decltype(CT), CT> constant;  // OK
    }
}

I don’t have a great example for this one, because I can’t immediately think of a situation where you’d want heterogeneity and be able to achieve constexprness. However, you can see what’s going on in this snippet: we make a constexpr std::tuple and then do something with each of its elements. P1306 proposes that the feature work sort of like structured binding:

void f3a() {
    constexpr std::tuple<int, float, unsigned long> tuple {42, true, 15uL};
    auto&& [args...] = tuple;
    for... (constexpr auto CT : args) {
        std::integral_constant<decltype(CT), CT> constant;  // OK
    }
}

except that of course you can’t say auto&& [args...] = tuple; in the working draft yet, either.

The other confusing thing about this third case is I wonder if you’re supposed to be able to mix and match among the conflated cases. For example:

template<class... Ts>
void f3b() {
    constexpr std::tuple<Ts...> tuple {42, true, 15uL};
    for... (constexpr Ts CT : tuple) {
        std::integral_constant<Ts, CT> constant;  // OK??
    }
}

Here CT’s type comes from a pack-expansion (Example 1), but its value comes from structured-binding on a tuple (Example 3).

Packs of packs, revisited

Because of its special case for constexpr tuple, P1306 hits a familiar problem: packs of packs.

template<class... Tuples>
void f4(Tuples... tuples) {  // A
    for... (auto elt : tuples) {
        do_something_with(elt);
    }
}

void use() {
    f4(std::make_tuple(1, 2.0f), std::make_tuple(3, 4.0f));
}

This clearly means to do_something_with({1, 2.0f}) and then do_something_with({3, 4.0f}). The mention of tuples on line A is referring to the pack, and elt is an element of the pack (that is, elt has type tuple<int, float>). But:

template<class... Tuples>
void f5(Tuples... tuples) {
    ([&]() {
        for... (auto elt : tuples) { // A
            do_something_with(elt);
        }
    }() , ...)
}

void use() {
    f5(std::make_tuple(1, 2.0f), std::make_tuple(3, 4.0f));
}

Now, the mention of tuples on line A must be referring to a single element of the pack (that is, tuples has type tuple<int, float>) and elt must be referring to an element of that tuple (that is, elt has type int or float).

I suspect that this can be used to create complicated ambiguities similarly to “Packs of packs” (2019-02-11), but I haven’t fully thought it out yet.

You could definitely remove all ambiguity by eliminating the special case for constexpr tuple, and un-conflating the cases for constexpr array and non-constexpr pack — that is, making the pack-expansion case use for... (but not constexpr because packs are never constexpr) and making the array case use for (constexpr auto i : arr). It is unclear to me how frequently programmers want to iterate over constexpr tuples.

Posted 2019-02-28