4

I often use initializer lists and for-each loops to iterate through a small number of ad-hoc values, like so:

for (auto x : {1, 2, 6, 24, 120}) {
  do_something(x);
}

I recently tried to write something similar, but with structured bindings and packed-together values instead:

for (auto[dx, dy] : {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}) {
  try_to_move(dx, dy); // nope! won’t compile
}

Unfortunately, this doesn’t compile. Clang tells me:

error: cannot use type ‘void’ as a range

In fact, even something like auto mylist = {{1, 2}, {3, 4}}; won’t compile.

This leaves me with two questions:

  1. Is there an alternative syntax to accomplish what I want in a terse and readable manner?

  2. Why doesn’t the type of auto mylist get parsed as initializer_list<initializer_list<int>>? Wouldn’t that work fine?

cigien
  • 57,834
  • 11
  • 73
  • 112
  • Platform? Compiler? – Severin Pappadeux Mar 25 '21 at 17:29
  • 3
    @SeverinPappadeux I fail to see how that matters for this question. It's a C++ language question, which is platform, and compiler independent. – cigien Mar 25 '21 at 17:30
  • Well, because `std::initializer_list> mylist = { {1, 2}, {3, 4} };` works. (and it should). It means type deduction in particular compiler might be buggy – Severin Pappadeux Mar 25 '21 at 17:35
  • 2
    @SeverinPappadeux Those are different things. You're specifying the type which is fine. OP is asking if that can be deduced from just `{ {}, ... }`, which is not actually possible. – cigien Mar 25 '21 at 17:39
  • 1
    Unrelated: You could give it some guideance and it should work: `{std::pair{-1, 0}, {1, 0}, {0, -1}, {0, 1}}` – Ted Lyngmo Mar 25 '21 at 17:47
  • "*I often use initializer lists and for-each loops to iterate through a small number of ad-hoc values*" That's not what braced-init-lists are for, which is why it only works in certain specific cases. – Nicol Bolas Mar 25 '21 at 17:57
  • @NicolBolas Would you mind elaborating on that? They’ve worked every time I’ve used them up until this case. – Presley Graham Mar 25 '21 at 18:20
  • 1
    @PresleyGraham: "Initializer lists" are so named because you're supposed to use them to *initialize* an object. You're not really using them to do that, so you're kind of misusing the feature. That's why it has weird corner cases where it doesn't work the way you want it to. – Nicol Bolas Mar 25 '21 at 18:25
  • @NicolBolas I see, thank you. Now that I know the thing I’m talking about is called a “braced-init-list”, I was able to find some of the posts that you’ve previously made on this topic, which were a tremendous help in clearing things up. Perhaps I should post this as a new question, but I’m wondering if I should stop using braced-init-lists in range-based for loops — though it’s not the intended purpose of the construct, it seems more readable and clear than alternatives. – Presley Graham Mar 25 '21 at 20:17
  • @NicolBolas: [`std::max`](https://en.cppreference.com/w/cpp/algorithm/max) has overloads taking `std::initializer_list` though. it is not reserved to initialization even in std. (might be an *"error"* to have allow that and `std::span` should be the right interface though). – Jarod42 Mar 26 '21 at 08:51

2 Answers2

4

Why doesn’t the type of auto mylist get parsed as initializer_list<initializer_list<int>>? Wouldn’t that work fine?

The reason is that simply no-one proposed it yet.

The handy syntax auto x = {1, 2, 6, 24, 120}; comes from proposal N3912 which was adopted into C++17 (see also N3922).

The deduction process is outlined in [dcl.type.auto.deduct]/4:

If the placeholder is the auto type-specifier, the deduced type T' replacing T is determined using the rules for template argument deduction. Obtain P from T by replacing the occurrences of auto with either a new invented type template parameter U or, if the initialization is copy-list-initialization, with std​::​initializer_­list<U>. Deduce a value for U using the rules of template argument deduction from a function call, where P is a function template parameter type and the corresponding argument is e. If the deduction fails, the declaration is ill-formed.

Since type deduction in a hypothetical function call f({1, 2}) would fail, so too is the nested braced-init-list deduction auto x = { {1, 2}, {3, 4} }; also impossible.

I guess the same trick could be applied to a deduction from a function call, which would make nested initializer_list deduction possible.

So a follow-up proposal is welcome.

Is there an alternative syntax to accomplish what I want in a terse and readable manner?

You could define a good old multidimensional array:

int lst[][2] = { {1, 2}, {3, 4}, {5, 6}, {7, 8} };
for (auto [x, y] : lst) {
    . . .
}

Or as suggested in the comments, give the first pair a type to help the deduction:

for (auto [x, y] : { std::pair{1, 2}, {3, 4}, {5, 6}, {7, 8} }) {
    . . .
}
rustyx
  • 80,671
  • 25
  • 200
  • 267
  • 1
    Note that an `initializer_list>` would *not* be able to be decomposible as requested, as it doesn't and cannot have the proper interface for structured binding – Nicol Bolas Mar 25 '21 at 21:00
  • 1
    Good point. I guess that covers the "*Wouldn’t that work fine?*" part of the question. More details on that here [1](https://stackoverflow.com/questions/44667517/why-does-stdinitializer-list-not-support-stdget-stdtuple-size-and-std), [2](https://stackoverflow.com/questions/44666169/initializer-list-and-structured-bindings-deduction-ambiguity-in-c17) – rustyx Mar 25 '21 at 21:21
0

You can use it like this. You just need to initialize your ad-hoc list to a variable first.

vector<pair<int, int> > p = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (auto x : p) {
    try_to_move(x);
}

You then access the parameter x like this in the function

<return-type> try_to_move(pair<int, int> x){
    int dx = x.first;
    int dy = x.second;
    // TO-DO
}
risingStark
  • 1,153
  • 10
  • 17