Время жизни возвращаемого значения 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 возникает только тогда, когда выражение возвращается, а список с расширенным набором команд не является выражением.
Ответы
Ответ 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
копию не ссылкой, но эта проблема по-прежнему сохраняется открытым и даже не предложили формулировки, не говоря уже о опыте внедрения.
Даже если ваш пример предназначен для работы, формулировка, касающаяся времени жизни базового массива, очевидно, все еще улучшается, и разработчикам потребуется некоторое время, чтобы реализовать окончательную семантику.