Каково объяснение исключений временного увеличения времени жизни объекта при привязке к ссылке?

В 12.2 стандарта C ++ 11:

Временный объект, к которому привязана ссылка, или временный объект, являющийся полным объектом подобъекта, к которому привязана ссылка, сохраняется в течение всего времени существования ссылки, за исключением:

  1. Временная привязка к ссылочному элементу в ctor-initializer конструктора (12.6.2) сохраняется до выхода из конструктора.

  2. Временная привязка к параметру ссылки в вызове функции (5.2.2) сохраняется до завершения полного выражения, содержащего вызов.

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

  4. Временная привязка к ссылке в новом инициализаторе (5.3.4) сохраняется до завершения полного выражения, содержащего новый инициализатор.

И в стандарте есть пример последнего случая:

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. имеет смысл, и с этим легко согласиться. Но в чем причина 1. and 4.? Мне этот пример кажется просто злым.


person updogliu    schedule 20.02.2014    source источник
comment
@Mehrdad: ссылка находится в структуре S. Что лично я нахожу странным, так это то, что стандарты говорят, что S a{ 1,{2,3} }; не вызывает проблем. Он даже не компилируется на моем компьютере (Mac OS 10.8 с Xcode 5)   -  person Kevin MOLCARD    schedule 21.02.2014
comment
@KevinMOLCARD: Ой, упс, я полностью пропустил эту ссылку в структуре, моя плохая ... удалил свой комментарий.   -  person user541686    schedule 21.02.2014
comment
Где-то должен быть другой дубликат, но лучшее, что я могу найти, - это заголовок stackoverflow.com/questions/5719636/… не очень хороший ответ.   -  person Potatoswatter    schedule 21.02.2014


Ответы (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» будет уничтожен до завершения функции.

Но в случае "нового 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 потребуется освободить эту кучу памяти! Это полностью противоречит поведению ссылок и нарушит все, что использует обычную семантику ссылок:

  // No way to implement this that satisfies case 2a but doesn't
  // break normal reference semantics.
  delete p;

Итак, ответ на ваш вопрос: правила определены таким образом, потому что это своего рода единственное практическое решение с учетом семантики C ++ в отношении времени жизни стека, кучи и объектов.

ПРЕДУПРЕЖДЕНИЕ: @Potatoswatter отмечает ниже, что это, похоже, не реализовано последовательно во всех компиляторах C ++ и, следовательно, в лучшем случае на данный момент не является переносимым. Посмотрите на его пример, как Clang не выполняет то, что, по-видимому, требует стандарт. Он также говорит, что ситуация «может быть более ужасной» - я не знаю точно, что это значит, но похоже, что на практике этот случай в C ++ имеет некоторую неопределенность.

person Josh Haberman    schedule 20.02.2014
comment
Похоже, что этот ответ будет выбран ... необходимо только отметить, что продление срока службы путем привязки к нестатической ссылке на член не переносимо. Я думаю, что ситуация более ужасная, но сейчас у меня нет доказательств. - person Potatoswatter; 21.02.2014
comment
@Potatoswattr: спасибо за информацию, я обновил свой ответ, добавив предупреждение об этом. - person Josh Haberman; 21.02.2014
comment
Я чувак. Более ужасный, чем это означает, что это не просто непереносимо, но в стандарте не сказано, что комитет имел в виду, поэтому любое понятие официальной поддержки ошибочно и будет длиться только до тех пор, пока стандарт не будет изменен, чтобы удалить его. - person Potatoswatter; 21.02.2014
comment
@Potatoswatter: это немного прискорбно, кажется полезным и достаточно простым в реализации. Может быть, это слишком сложно / тонко, чтобы полагаться на такое поведение в реальных программах? - person Josh Haberman; 21.02.2014
comment
Я не уверен в простоте реализации; Я еще не надел защитный шлем компилятора, но не думаю, что решение приемлемо. Например, для временной привязки к объекту возвращаемого значения, время жизни которого продлевается вызывающей стороной, потребуется продление времени жизни. - person Potatoswatter; 22.02.2014

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

Когда вы вызываете функцию, она расширяется до конца текущей строки. Это достаточно долго, и его легко определить.

Когда вы создаете автоматическую ссылку на хранилище «в стеке», объем этой автоматической ссылки на хранилище может быть определен детерминированно. На этом этапе временное можно очистить. (По сути, создайте анонимную автоматическую переменную хранения для хранения временного)

В выражении new точка разрушения не может быть статически определена в точке создания. Это всякий раз, когда встречается delete. Если бы мы хотели, чтобы delete (иногда) уничтожал временное, тогда наша эталонная «двоичная» реализация должна быть более сложной, чем указатель, а не меньше или равно. Иногда ему принадлежат упомянутые данные, а иногда нет. Итак, это указатель плюс bool. А в C ++ вы не платите за то, что не используете.

То же самое и в конструкторе, потому что вы не можете знать, был ли конструктор в new или в стеке. Таким образом, любое продление срока службы не может быть статически понято на рассматриваемой линии.

person Yakk - Adam Nevraumont    schedule 20.02.2014

Как долго вы хотите, чтобы временный объект прослужил? Его нужно где-то разместить.

Его не может быть в куче, потому что он протечет; нет применимого автоматического управления памятью. Он не может быть статичным, потому что их может быть больше одного. Он должен быть в стеке. Затем он длится либо до конца выражения, либо до конца функции.

Другие временные элементы в выражении, возможно связанные с параметрами вызова функции, уничтожаются в конце выражения и сохраняются до конца функции или области «{}», что будет исключением из общих правил. Таким образом, путем дедукции и экстраполяции других случаев, полное выражение является наиболее разумным сроком службы.

Я не уверен, почему вы говорите, что это не проблема:

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

Объект, привязанный к ссылке на член класса, не имеет расширенного срока службы.

На самом деле я уверен, что это проблема известного дефекта в стандарте, но сейчас у меня нет времени углубляться ...

person Potatoswatter    schedule 20.02.2014
comment
Висячая ссылка одна и та же, независимо от того, используете ли вы new или нет - я не верю, что это правда, @updogliu специально цитирует язык из стандарта, что в этом случае время жизни временного продлевается. - person Josh Haberman; 21.02.2014
comment
@JoshHaberman Я вижу четыре случая, и ни один из них здесь не применим. Хотите уточнить? - person Potatoswatter; 21.02.2014
comment
Это не один из четырех случаев. Процитированное правило гласит, что время жизни временной привязки к ссылке продлевается до времени жизни ссылки , если не применяется один из этих случаев. - person Josh Haberman; 21.02.2014
comment
Объект, привязанный к ссылке на член класса, не имеет расширенного срока службы. Это удивительно. В других случаях привязка const ref действительно продлевает время жизни, как и следовало ожидать из приведенной выше цитаты из стандарта: gist.github.com/anonymous/9125173 - person Josh Haberman; 21.02.2014
comment
@Potatoswatter Какая версия clang? Я получаю разные результаты как от clang, так и от gcc. И мне кажется, что эти 4 случая являются исключениями из правила продления срока службы, и, поскольку ни один из них не применим к первому примеру, время жизни этого временного элемента действительно увеличивается. Или я как-то ошибся? - person Praetorian; 21.02.2014
comment
@Praetorian Действительно, я специально выбрал данные для этого ответа. Я уверен, что это было описано где-то в другом месте на SO, но я ничего не могу найти или официального DR. У меня сейчас нет на это времени. Возможно, это просто ошибка Clang, но в любом случае использование нестатической ссылки на член для продления срока службы непереносимо. - person Potatoswatter; 21.02.2014