Странные значения в lambda, возвращающем initializer_list
Рассмотрим этот С++ 11 фрагмент кода:
#include <iostream>
#include <set>
#include <stdexcept>
#include <initializer_list>
int main(int argc, char ** argv)
{
enum Switch {
Switch_1,
Switch_2,
Switch_3,
Switch_XXXX,
};
int foo_1 = 1;
int foo_2 = 2;
int foo_3 = 3;
int foo_4 = 4;
int foo_5 = 5;
int foo_6 = 6;
int foo_7 = 7;
auto get_foos = [=] (Switch ss) -> std::initializer_list<int> {
switch (ss) {
case Switch_1:
return {foo_1, foo_2, foo_3};
case Switch_2:
return {foo_4, foo_5};
case Switch_3:
return {foo_6, foo_7};
default:
throw std::logic_error("invalid switch");
}
};
std::set<int> foos = get_foos(Switch_1);
for (auto && foo : foos) {
std::cout << foo << " ";
}
std::cout << std::endl;
return 0;
}
Какой бы компилятор я ни старался, все, похоже, неправильно обрабатывают его. Это заставляет меня думать, что я делаю что-то неправильно, а не обычную ошибку для нескольких компиляторов.
clang 3.5 вывод:
-1078533848 -1078533752 134518134
gcc 4.8.2 вывод:
-1078845996 -1078845984 3
gcc 4.8.3 вывод (скомпилирован на http://www.tutorialspoint.com):
1 2 267998238
gcc (неизвестная версия) вывод (скомпилирован на http://coliru.stacked-crooked.com)
-1785083736 0 6297428
Проблема, по-видимому, вызвана использованием std::initializer_list<int>
в качестве возвращаемого значения лямбда. При изменении лямбда-определения до [=] (Switch ss) -> std::set<int> {...}
возвращаемые значения верны.
Пожалуйста, помогите мне решить эту тайну.
Ответы
Ответ 1
От: http://en.cppreference.com/w/cpp/utility/initializer_list
Базовый массив не гарантированно существует после того, как закончился срок жизни исходного объекта списка инициализаторов. Хранилище для std:: initializer_list неуказано (то есть оно может быть автоматической, временной или статической постоянной памятью в зависимости от ситуации).
Я не думаю, что список инициализаторов можно скопировать. std::set
и другие контейнеры. В принципе, похоже, что ваш код ведет себя аналогично "возврату ссылки на временный".
У С++ 14 есть что-то немного отличное, чтобы сказать о базовом хранилище, продлевая его время жизни, но это не фиксирует ничего, связанного с временем жизни объекта initializer_list
, не говоря уже о его копиях. Следовательно, проблема остается, даже в С++ 14.
Базовый массив - это временный массив, в котором каждый элемент инициализируется копией (за исключением того, что сужение конверсий является недопустимым) из соответствующего элемента исходного списка инициализаторов. Время жизни базового массива совпадает с любым другим временным объектом , за исключением того, что инициализация объекта initializer_list из массива продлевает время жизни массива точно так же, как привязка ссылки к временному (с теми же исключениями, например, для инициализации нестатического члена класса). Основной массив может быть выделен в постоянной памяти.
Ответ 2
Проблема в том, что вы ссылаетесь на объект, который больше не существует, и поэтому вы вызываете undefined поведение. initializer_list
кажется underspecified в стандартном проекте С++ 11, нет нормативных разделов, которые фактически определяют это поведение. Хотя есть много заметок, которые указывают, что это не сработает, и в целом, хотя примечания не являются нормативными, если они не противоречат нормативному тексту, они являются очень показательными.
Если мы перейдем к разделу 18.9
списка инициализаторов, у него есть заметка, в которой говорится:
Копирование списка инициализаторов не копирует базовые элементы.
и в разделе 8.5.4
мы имеем следующие примеры:
typedef std::complex<double> cmplx;
std::vector<cmplx> v1 = { 1, 2, 3 };
void f() {
std::vector<cmplx> v2{ 1, 2, 3 };
std::initializer_list<int> i3 = { 1, 2, 3 };
}
со следующими примечаниями:
Для v1 и v2 объект initializer_list и массив, созданные для {1, 2, 3}, имеют полное выражение продолжительность жизни. Для i3 объект и массив initializer_list имеют автоматическое время жизни.
Эти примечания согласуются с предложением initializer_list: N2215, которое дает следующий пример:
std::vector<double> v = {1, 2, 3.14};
и говорит:
Теперь добавьте vector(initializer_list<E>)
в vector<E>
, как показано выше. Теперь, пример работает. Список инициализаторов {1, 2, 3.14} интерпретируется как временное построенное следующим образом:
const double temp[] = {double(1), double(2), 3.14 } ;
initializer_list<double> tmp(temp,
sizeof(temp)/sizeof(double));
vector<double> v(tmp);
[...]
Обратите внимание, что initializer_list - это маленький объект (возможно, два слова), поэтому передача его по значению имеет смысл. Передача по значению также упрощает вставка оценки begin() и end() и постоянной экспрессии размер().
Инициализатор_list будет создан компилятором, но может быть скопированных пользователями. Подумайте об этом как о двух указателях.
initializer_list
в этом случае просто содержит указатели на автоматическую переменную, которая не будет существовать после выхода из области.
Обновить
Я только понял, что это предложение действительно указывает на этот сценарий неправильного использования:
Одним из следствий является то, что initializer_list является "указателем типа" в этом он ведет себя как указатель относительно базового массива. Для Пример:
int * f(int a)
{
int* p = &a;
return p; //bug waiting to happen
}
initializer_list<int> g(int a, int b, int c)
{
initializer_list<int> v = { a, b, c };
return v; // bug waiting to happen
}
На самом деле требуется незначительная изобретательность, чтобы злоупотреблять initializer_list таким образом. В частности, переменные типа initializer_list будет редкостью.
Я считаю последнее выражение (особое внимание) особенно ироничным.
Обновление 2
Итак, отчет о дефекте 1290 исправляет нормативную формулировку, и поэтому теперь он охватывает это поведение, хотя случай копирования может быть более явным. В нем говорится:
Возник вопрос по ожидаемому поведению, когда initializer_list является нестационарным членом данных класса. Инициализация initializer_list определяется с точки зрения построения из неявно выделенный массив, время жизни которого "совпадает с временем жизни initializer_list object". Это означает, что массив должен жить до тех пор, как это делает initializer_list, что на первый взгляд кажется, требует, чтобы массив хранился в чем-то вроде std:: unique_ptr в том же классе (если член инициализируется таким образом).
Было бы удивительно, если бы это было намерение, но это сделало бы initializer_list, который можно использовать в этом контексте.
Разрешение фиксирует формулировку, и мы можем найти новую формулировку в N3485 версии проекта стандарта. Итак, раздел 8.5.4
[dcl.init.list] теперь говорит:
Массив имеет такое же время жизни, что и любой другой временный объект (12.2), за исключением того, что инициализация объекта initializer_- list из массива продлевает время жизни массива точно так же, как привязка ссылки к временный.
и 12.2
[class.temporary] говорит:
Время жизни временной привязки к возвращаемому значению в функции return (6.6.3) не распространяется; временное уничтожается в конце полного выражения в операторе return.
Ответ 3
Итак, initializer_list
не продлевают время жизни своего ссылочного массива, когда они сами копируются или перемещаются в результат копирования/перемещения. Это делает их проблематичными. (они продлевают время жизни ссылочного массива на собственное время жизни, но это расширение не является транзитивным по сравнению с разрешением или копиями списка).
Чтобы устранить эту проблему, сохраните данные и управляйте их ресурсом вручную:
template<size_t size, class T>
std::array<T, size> partial_array( T const* begin, T const* end ) {
std::array<T, size> retval;
size_t delta = (std::min)( size, end-begin );
end = begin+delta;
std::copy( begin, end, retval.begin() );
return retval;
}
template<class T, size_t max_size>
struct capped_array {
std::array<T, max_size> storage;
size_t used = 0;
template<size_t osize, class=std::enable_if_t< (size<=max_size) >>
capped_array( std::array<T, osize> const& rhs ):
capped_array( rhs.data(), rhs.data()+osize )
{}
template<size_t osize, class=std::enable_if_t< (size<=max_size) >>
capped_array( capped_array<T, osize> const& rhs ):
capped_array( rhs.data(), rhs.data()+rhs.used )
{}
capped_array(capped_array const& o)=default;
capped_array(capped_array & o)=default;
capped_array(capped_array && o)=default;
capped_array(capped_array const&& o)=default;
capped_array& operator=(capped_array const& o)=default;
capped_array& operator=(capped_array & o)=default;
capped_array& operator=(capped_array && o)=default;
capped_array& operator=(capped_array const&& o)=default;
// finish-start MUST be less than max_size, or we will truncate
capped_array( T const* start, T const* finish ):
storage( partial_array(start, finish) ),
used((std::min)(finish-start, size))
{}
T* begin() { return storage.data(); }
T* end() { return storage.data()+used; }
T const* begin() const { return storage.data(); }
T const* end() const { return storage.data()+used; }
size_t size() const { return used; }
bool empty() const { return !used; }
T& front() { return *begin(); }
T const& front() const { return *begin(); }
T& back() { return *std::prev(end()); }
T const& back() const { return *std::prev(end()); }
capped_array( std::initializer_list<T> il ):
capped_array(il.begin(), il.end() )
{}
};
Цель здесь проста. Создайте тип данных на основе стека, который хранит связку T
s, вплоть до кепки, и может обрабатывать меньше.
Теперь мы заменим ваш std::initializer_list
на:
auto get_foos = [=] (Switch ss) -> capped_array<int,3> {
switch (ss) {
case Switch_1:
return {foo_1, foo_2, foo_3};
case Switch_2:
return {foo_4, foo_5};
case Switch_3:
return {foo_6, foo_7};
default:
throw std::logic_error("invalid switch");
}
};
и ваш код работает. Свободный магазин не используется (без размещения кучи).
Более продвинутая версия будет использовать массив неинициализированных данных и вручную построить каждый T
.