Каково обоснование исключений временного расширения времени жизни объекта при привязке к ссылке?
в 12.2 стандарта C++11:
временный, к которому привязана ссылка или что полное объект подобъект, к которому привязана ссылка сохраняется в течение всего срока ссылки, за исключением:
временная граница к ссылочному члену в ctor-инициализаторе конструктора (12.6.2) сохраняется до завершения работы конструктора.
временная привязка к ссылка параметр в вызове функции (5.2.2) сохраняется до завершения полного выражения, содержащего вызов.
продолжительность жизни временной привязки к возвращаемому значению в функции return заявление (6.6.3) не продлевается; временное уничтожается на конец полного выражения в операторе return.
временный привязка к ссылке в новом инициализаторе (5.3.4) сохраняется до тех пор, пока завершение полное выражение, содержащее инициализатор new.
и есть пример последнего случая в стандарте:
struct S {
int mi;
const std::pair<int,int>& mp;
};
S a { 1,{2,3} }; // No problem.
S* p = new S{ 1, {2,3} }; // Creates dangling reference
ко мне, 2. and 3.
имеет смысл и легко договориться. Но в чем причина bebind 1. and 4.
? Этот пример кажется мне просто злым.
3 ответов
как и многие вещи в C и C++, я думаю, что это сводится к тому, что может быть разумно (эффективно) реализовать.
временные файлы обычно выделяются в стеке, а код для вызова их конструкторов и деструкторов испускается в саму функцию. Поэтому, если мы развернем ваш первый пример в том, что на самом деле делает компилятор, это будет выглядеть примерно так:
struct S {
int mi;
const std::pair<int,int>& mp;
};
// Case 1:
std::pair<int,int> tmp{ 2, 3 };
S a { 1, tmp };
компилятор может легко продлить срок службы tmp
временные достаточно долго, чтобы держите" S " действительным, потому что мы знаем, что "S" будет уничтожен до конца функции.
но это не работает в случае "new S":
struct S {
int mi;
const std::pair<int,int>& mp;
};
// Case 2:
std::pair<int,int> tmp{ 2, 3 };
// Whoops, this heap object will outlive the stack-allocated
// temporary!
S* p = new S{ 1, tmp };
чтобы избежать висячей ссылки, нам нужно было бы выделить временное в куче вместо стека, что-то вроде:
// Case 2a -- compiler tries to be clever?
// Note that the compiler won't actually do this.
std::pair<int,int> tmp = new std::pair<int,int>{ 2, 3 };
S* p = new S{ 1, tmp };
но тогда соответствующие delete p
нужно, чтобы освободить эту память! Это совершенно противоречит поведению ссылок и нарушит все, что использует normal справочная семантика:
// No way to implement this that satisfies case 2a but doesn't
// break normal reference semantics.
delete p;
таким образом, ответ на ваш вопрос: правила определены таким образом, потому что это своего рода единственное практическое решение, учитывая семантику C++вокруг стека, кучи и времени жизни объекта.
предупреждение: @Potatoswatter отмечает ниже, что это, похоже, не реализуется последовательно в компиляторах C++ и, следовательно, не является переносимым в лучшем случае на данный момент. См. его пример для того, как Clang не делает то, что стандарт кажется мандат здесь. Он также говорит, что ситуация "может быть более ужасной" - я не знаю точно, что это означает, но похоже, что на практике этот случай в C++ имеет некоторую неопределенность.
основной упор заключается в том, что расширение ссылки происходит только тогда, когда время жизни может быть легко и детерминированно определено, и этот факт может быть выведен как можно в строке кода, где создается временная.
при вызове функции она расширяется до конца текущей строки. Это достаточно долго, и легко определить.
когда вы создаете ссылку автоматического хранения "в стеке", область этого автоматического хранения ссылка может быть детерминированно определена. Временное может быть очищено в этот момент. (В принципе, создайте анонимную автоматическую переменную хранения для хранения временной)
на new
выражение, точка разрушения не может быть статически определена в точке создания. Это когда delete
происходит. Если бы мы хотели delete
чтобы (иногда) уничтожить временное, тогда наша ссылочная "двоичная" реализация должна быть более сложной, чем указатель, а не меньше или равно. Иногда он будет владеть указанными данными, а иногда нет. Так что это указатель, плюс bool
. И в C++ вы не платите за то, что не используете.
то же самое относится к конструктору, потому что вы не можете знать, был ли конструктор в new
или выделение стека. Таким образом, любое продление жизни не может быть статически понято на рассматриваемой линии.
как долго вы хотите, чтобы временный объект к последнему? Он должен быть где-то выделен.
Он не может быть в куче, потому что он будет протекать; нет применимого автоматического управления памятью. Он не может быть статичным, потому что его может быть несколько. Он должен быть в стопке. Затем он либо длится до конца выражения, либо до конца функции.
другие временные значения в выражении, возможно, связанные с параметрами вызова функции, уничтожаются в конце выражение, и сохраняется до конца функции или"{}
" область применения будет исключением из общих правил. Таким образом, путем дедукции и экстраполяции других случаев полное выражение является наиболее разумным сроком жизни.
Я не уверен, почему вы говорите, что это не проблема:
S a { 1,{2,3} }; // No problem.
висячая ссылка такая же, используете ли вы new
.
инструментирование вашей программы и запуск ее в Clang производит следующее результаты:
#include <iostream>
struct noisy {
int n;
~noisy() { std::cout << "destroy " << n << "\n"; }
};
struct s {
noisy const & r;
};
int main() {
std::cout << "create 1 on stack\n";
s a {noisy{ 1 }}; // Temporary created and destroyed.
std::cout << "create 2 on heap\n";
s* p = new s{noisy{ 2 }}; // Creates dangling reference
}
create 1 on stack
destroy 1
create 2 on heap
destroy 2
объект, привязанный к ссылке члена класса, не имеет расширенного срока службы.
на самом деле я уверен, что это предмет известный дефект в стандарте, но у меня нет времени вникать сейчас...