время жизни возвращаемого значения 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 по примеру. Clang также правильно обрабатывает пример, но объекты в списке никогда уничтожены; это может привести к утечке памяти. ICC не поддерживает initializer_list совсем.

Мой анализ правильный?


C ++ 11 §6.6.3 / 2:

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

8.5.4 / 1:

… Инициализация списка в контексте инициализации копирования называется копирование списка инициализация.

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 элементы типа Е, где 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 и массив имеют автоматическое время жизни. — конец примера]


Небольшое разъяснение о возврате списка в скобках

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

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

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

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

nocopy f() {
return { 3 };
}

это не:

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

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

23

Решение

Формулировка, на которую вы ссылаетесь в 8.5.4 / 6, неверна и была исправлена ​​(несколько) DR1290. Вместо того чтобы говорить:

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

… измененный стандарт теперь гласит:

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

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

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

Следовательно noisydt объекты уничтожаются до возврата функции.

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

destroyed
destroyed
destroyed
received
destroyed
destroyed
destroyed
received

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

12

Другие решения

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

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

16