Реализация 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 возникает только тогда, когда выражение возвращается, а список с расширенным набором команд не является выражением.