Why I can not return initializer list from lambda
Asked Answered
G

3

29

Why this code is not valid?

auto foo = [] () {
    return {1, 2};     
};

However, this is valid since the initializer_list is used just to initialize a vector not to return itself:

auto foo = [] () -> std::vector<int> {
    return {1, 2};     
};

Why I can not return initializer_list? It could be useful. For example, a lambda that can be used to initialize a vector or a list or ... with some default values for something.

Geri answered 12/2, 2016 at 11:25 Comment(4)
std::initializer_list has reference semantics, therefore the thing you return would immediately dangle.Hamner
@T.C Thanks. I did not understand. but I think there is enough keywords to search for.Geri
@Hamner Really? I thought it copy-initialized each element as if it was an array. Do you have a link I can look at or standards quote?Public
@Public eel.is/c++draft/dcl.init.list#6Hamner
H
35

Lambda return type deduction uses the auto rules, which normally would have deduced std::initializer_list just fine. However, the language designers banned deduction from a braced initializer list in a return statement ([dcl.spec.auto]/7):

If the deduction is for a return statement and the initializer is a braced-init-list ([dcl.init.list]), the program is ill-formed.

The reason for this is that std::initializer_list has reference semantics ([dcl.init.list]/6).
[]() -> std::initializer_list<int> { return {1, 2}; } is every bit as bad as
[]() -> const int & { return 1; }. The lifetime of the backing array of the initializer_list object ends when the lambda returns, and you are left with a dangling pointer (or two).

Demo:

#include <vector>

struct Noisy {
    Noisy()  { __builtin_printf("%s\n", __PRETTY_FUNCTION__); }
    Noisy(const Noisy&) { __builtin_printf("%s\n", __PRETTY_FUNCTION__); }
    ~Noisy() { __builtin_printf("%s\n", __PRETTY_FUNCTION__); }
};

int main()
{
    auto foo = []() -> std::initializer_list<Noisy> { return {Noisy{}, Noisy{}}; };
    std::vector<Noisy> bar{foo()};
}

Output:

Noisy::Noisy()
Noisy::Noisy()
Noisy::~Noisy()
Noisy::~Noisy()
Noisy::Noisy(const Noisy&)
Noisy::Noisy(const Noisy&)
Noisy::~Noisy()
Noisy::~Noisy()

Note how the copy constructors are called after all the Noisy objects created so far have been destroyed already.

Hamner answered 12/2, 2016 at 11:41 Comment(2)
So it is simply not valid to do this at all.. Thanks sir for explanation.Geri
@HumamHelfawi Your vector version works fine, but is not ideal. I'd consider returning an std::array which seems to be close to what you want, except you have to manually count the number of objects. Maybe you can get something done with constexpr to automatically extract the array size.Underlinen
H
7

std::initializer_list can't be deduced by a template argument, which means you'll have to tell the lambda what it is explicitly:

#include <initializer_list>
#include <iostream>
#include <vector>

int main()
{
    auto foo = []() -> std::initializer_list<int> { return {1, 2}; };
    std::vector<int> bar{foo()};
    for (int x : bar) { std::cout << x << "  "; };
}

Demo. Here's the rationale behind this from the initializer list proposal:

Can an initializer list be used as a template argument? Consider:

template<class T> void f(const T&);
f({ }); // error
f({1});
f({1,2,3,4,5,6});
f({1,2.0}); // error
f(X{1,2.0}); // ok: T is X

There is obviously no problem with the last call (provided X{1,2.0} itself is valid)
because the template argument is an X. Since we are not introducing arbitrary lists of
types (product types), we cannot deduce T to be {int,double} for f({1,2.0}), so that call is
an error. Plain {} does not have a type, so f({}) is also an error.

This leaves the homogeneous lists. Should f({1}) and f({1,2,3,4,5,6}) be accepted? If so,
with what meaning? If so, the answer must be that the deduced type, T, is
initializer_list. Unless someone comes up with at least one good use of this simple
feature (a homogeneous list of elements of type E is deduced to be an
initializer_list), we won’t propose it and all the examples will be errors: No template
argument can be deduced from an (unqualified) initializer list. One reason to be cautious
here is that we can imagine someone getting confused about the possible interpretations
of single-element lists. For example, could f({1}) invoke f<int>(1)? No, that would be
quite inconsistent.

Horwath answered 12/2, 2016 at 11:29 Comment(4)
Thanks. But so strange.. it's just a problem of auto deduced! Are you sure that it is totaly valid and no UB or any other problem could be happen here? becuase i have doubt that it is valid but just a deduction problem..Geri
@HumamHelfawi Yes, the standard explicitly says std::initializer_list can't be type-deduced. Here's an answer that goes into more details.Horwath
Do not confuse std::initializer_list with a braced init listConventual
@HumamHelfawi Found the rationale from the proposal, should make things clearer for you.Horwath
L
2

You can return an initializer_list from the function either like that:

return std::initializer_list<int>{1, 2};

or

auto ret = {1, 2};
return ret;

The reason is, that the auto variable declaration uses different rules than the auto return type deduction. The first one has a special rule for this case, and the second one uses plain template type deduction.

This is discussed at length in Scott Meyers Effective Modern C++, Item 2. There is also a video and slides from him about the topic.

Lactoflavin answered 12/2, 2016 at 11:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.