Подтвердить что ты не робот

Странные значения в 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> {...} возвращаемые значения верны.

Пожалуйста, помогите мне решить эту тайну.

4b9b3361

Ответ 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.