Почему я не могу вернуть список инициализаторов из лямбда

Почему этот код недействителен?

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

Однако это справедливо, так как initializer list используется для инициализации vector не для возврата:

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

Почему я не могу вернуть initializer list? Это может быть полезно. Например, лямбда, которая может быть использована для инициализации vector или list или... с некоторыми значениями по умолчанию для чего-то.

Ответ 1

Вывод типа возврата Lambda использует правила auto, которые, как правило, вывели бы std::initializer_list просто отлично. Тем не менее, разработчики языка запретили вывод из скопированного списка инициализаторов в операторе return ([dcl.spec.auto]/7):

Если вывод для оператора return, а инициализатор - braced-init-list ([dcl.init.list]), программа плохо сформирована.

Причиной этого является то, что std::initializer_list имеет ссылочную семантику ([dcl.init.list]/6).
[]() -> std::initializer_list<int> { return {1, 2}; } - это все так же плохо, как
[]() -> const int & { return 1; }. Время жизни массива поддержки объекта initializer_list заканчивается, когда возвращается лямбда, и вы остаетесь с висящим указателем (или двумя).

Демо:

#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()};
}

Вывод:

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

Обратите внимание, как вызываются конструкторы копий после того, как все созданные объекты Noisy уже были уничтожены.

Ответ 2

std::initializer_list не может быть выведен аргументом шаблона, а это означает, что вам нужно указать лямбда, что это явно:

#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 << "  "; };
}

Демо. Здесь обоснование этого из предложения списка инициаторов:

Можно ли использовать список инициализаторов в качестве аргумента шаблона? Рассмотрим:

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

Очевидно, что нет проблемы с последним вызовом (при условии, что действительно X {1,2.0})
потому что аргумент шаблона X. Поскольку мы не вводим произвольные списки (типы продуктов), мы не можем вывести T как {int, double} для f ({1,2.0}), так что вызов - ошибка. Plain {} не имеет типа, поэтому f ({}) также является ошибкой.

Это оставляет однородные списки. Должны быть приняты f ({1}) и f ({1,2,3,4,5,6})? Если это так,
с каким значением? Если это так, то ответ должен заключаться в том, что выведенный тип T - это initializer_list. Если кто-то не придумает хотя бы одно хорошее использование этого простого
(однородный список элементов типа E выводится как initializer_list), мы не предложим его, и все примеры будут ошибками: Нет шаблона
аргумент может быть выведен из (неквалифицированного) списка инициализаторов. Одна из причин быть осторожными
здесь мы можем представить, что кто-то путается о возможных интерпретациях одноэлементных списков. Например, может f ({1}) вызывать f <int> (1)? Нет, это будет совершенно непоследовательно.

Ответ 3

Вы можете вернуть initializer_list из функции так:

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

или

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

Причина в том, что в объявлении переменной auto используются разные правила, кроме вычитания типа auto. Первое имеет специальное правило для этого случая, а второе использует простой вывод типа шаблона.

Это подробно обсуждается в Scott Meyers Effective Modern С++, пункт 2. Существует также видео и слайд от него о теме.