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

Время жизни возвращаемого значения std:: initializer_list

Реализация GCC уничтожает массив std::initializer_list, возвращаемый функцией в конце возвращаемого полного выражения. Правильно ли это?

Оба тестовых примера в этой программе показывают деструкторы, выполняемые до того, как значение может быть использовано:

#include <initializer_list>
#include <iostream>

struct noisydt {
    ~noisydt() { std::cout << "destroyed\n"; }
};

void receive( std::initializer_list< noisydt > il ) {
    std::cout << "received\n";
}

std::initializer_list< noisydt > send() {
    return { {}, {}, {} };
}

int main() {
    receive( send() );
    std::initializer_list< noisydt > && il = send();
    receive( il );
}

Я думаю, что программа должна работать. Но базовый стандарт немного запутан.

Оператор return инициализирует объект возвращаемого значения, как если бы он был объявлен

std::initializer_list< noisydt > ret = { {},{},{} };

Это инициализирует один временный initializer_list и его базовое хранилище массива из данной серии инициализаторов, а затем инициализирует другой initializer_list из первого. Каково время жизни массива? "Время жизни массива такое же, как у объекта initializer_list". Но есть два из них; который неоднозначен. Пример в 8.5.4/6, если он работает как рекламируемый, должен устранить двусмысленность в том, что массив имеет время жизни скопированного объекта. Тогда массив возвращаемых значений также должен выжить в вызывающей функции, и его можно сохранить, привязывая его к именованной ссылке.

В LWS GCC ошибочно убивает массив перед возвратом, но он сохраняет имя initializer_list для примера. Кланг также корректно обрабатывает этот пример, но объекты в списке никогда не уничтожаются; это приведет к утечке памяти. ICC вообще не поддерживает initializer_list.

Правильно ли мой анализ?


С++ 11 §6.6.3/2:

Оператор return с бинтом-init-list инициализирует объект или ссылку, которые будут возвращены из функции путем инициализации-списка-инициализации (8.5.4) из указанного списка инициализаторов.

8.5.4/1:

... инициализация списка в контексте инициализации копирования называется copy-list-initialization.

8,5/14:

Инициализация, которая встречается в форме T x = a;..., называется копией-инициализацией.

Вернуться к разделу 8.5.4/3:

Список-инициализация объекта или ссылки типа T определяется следующим образом:...

- В противном случае, если T является специализацией std::initializer_list<E>, объект initializer_list строится, как описано ниже, и используется для инициализации объекта в соответствии с правилами инициализации объекта из класса того же типа (8.5).

8.5.4/5:

Объект типа std::initializer_list<E> создается из списка инициализаторов, как если бы реализация выделила массив из N элементов типа E, где N - количество элементов в списке инициализаторов. Каждый элемент этого массива инициализируется копией с соответствующим элементом списка инициализаторов, а объект std::initializer_list<E> создается для обращения к этому массиву. Если для инициализации любого из элементов требуется сужение преобразования, программа плохо сформирована.

8.5.4/6:

Время жизни массива такое же, как у объекта initializer_list. [Пример:

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 имеют автоматическое время жизни. - конец примера]


Небольшое пояснение о возврате списка с привязкой-init-list

Когда вы возвращаете пустой список, заключенный в фигурные скобки,

Оператор return с бинтом-init-list инициализирует объект или ссылку, которые будут возвращены из функции путем инициализации-списка-инициализации (8.5.4) из указанного списка инициализаторов.

Это не означает, что объект, возвращаемый в область вызова, копируется из чего-то. Например, это действительно:

struct nocopy {
    nocopy( int );
    nocopy( nocopy const & ) = delete;
    nocopy( nocopy && ) = delete;
};

nocopy f() {
    return { 3 };
}

это не:

nocopy f() {
    return nocopy{ 3 };
}

Инициализация списка копий просто означает, что эквивалент синтаксиса nocopy X = { 3 } используется для инициализации объекта, представляющего возвращаемое значение. Это не вызывает копию, и это бывает так же, как пример расширения продолжительности массива 8.5.4/6.

И Clang и GCC согласны по этому вопросу.


Другие примечания

В обзоре N2640 не упоминается об этом случае в углу. Здесь подробно обсуждались индивидуальные особенности, но я ничего не вижу об их взаимодействии.

Реализация этого получает волосатый, поскольку он сводится к возврату необязательного массива переменной длины по значению. Поскольку std::initializer_list не имеет своего содержимого, функция должна также возвращать что-то другое, что делает. При переходе к функции это просто локальный массив фиксированного размера. Но в другом направлении VLA необходимо вернуть в стек вместе с указателями std::initializer_list. Затем вызывающему нужно сообщить, нужно ли распоряжаться последовательностью (независимо от того, находятся ли они в стеке или нет).

Проблема очень легко наткнуться, возвращая список с фиксированным списком из лямбда-функции, как "естественный" способ вернуть несколько временных объектов, не заботясь о том, как они содержатся.

auto && il = []() -> std::initializer_list< noisydt >
               { return { noisydt{}, noisydt{} }; }();

Действительно, это похоже на то, как я сюда приехал. Но было бы ошибкой исключить тип -> trailing-return-type, потому что вывод типа lambda return возникает только тогда, когда выражение возвращается, а список с расширенным набором команд не является выражением.

4b9b3361

Ответ 1

Текст, указанный в 8.5.4/6, является дефектным и был исправлен (несколько) с помощью DR1290. Вместо того, чтобы говорить:

Время жизни массива такое же, как у объекта initializer_list.

... измененный стандарт теперь говорит:

Массив имеет такое же время жизни, что и любой другой временный объект (12.2 [class.temporary]), за исключением того, что инициализация объекта initializer_list из массива продлевает время жизни массива точно так же, как привязка ссылки к временному.

Поэтому управляющая формулировка времени жизни временного массива 12.2/5, в котором говорится:

Время жизни временной привязки к возвращаемому значению в операторе return функции не распространяется; временное уничтожается в конце полного выражения в заявлении return

Поэтому объекты noisydt уничтожаются до возвращения функции.

До недавнего времени у Клана была ошибка, из-за которой в некоторых случаях он не смог уничтожить базовый массив для объекта initializer_list. Я исправил это для Clang 3.4; выход для вашего тестового случая из соединительной линии Clang:

destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received

... что правильно, на DR1290.

Ответ 2

std::initializer_list не является контейнером, не используйте его для передачи значений вокруг и ожидайте, что они будут сохраняться

DR 1290 изменил формулировку, вы также должны знать 1565 и 1599, которые еще не готовы.

Затем массив возвращаемых значений также должен выжить в вызывающей функции, и его можно сохранить, привязывая его к именованной ссылке.

Нет, это не следует. Время жизни массива не расширяется вместе с initializer_list. Рассмотрим:

struct A {
    const int& ref;
    A(const int& i = 0) : ref(i) { }
};

Ссылка i привязывается к временному int, а затем привязка к нему также привязывается к ref, но это не продлевает время жизни i, оно по-прежнему выходит из области видимости конец конструктора, оставив свисающую ссылку. Вы не распространяете базовое временное время жизни, связывая с ним другую ссылку.

Ваш код может быть более безопасным, если 1565 одобрен, и вы делаете il копию не ссылкой, но эта проблема по-прежнему сохраняется открытым и даже не предложили формулировки, не говоря уже о опыте внедрения.

Даже если ваш пример предназначен для работы, формулировка, касающаяся времени жизни базового массива, очевидно, все еще улучшается, и разработчикам потребуется некоторое время, чтобы реализовать окончательную семантику.