Error returning reference to temporary

GCC returning reference to temporary C++ Решение и ответ на вопрос 995859

gromo

382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

1

02.11.2013, 23:53. Показов 4374. Ответов 19

Метки нет (Все метки)


Например вот так:

C++ (Qt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int func2()
{
    int input;
    cin >> input;
    return input;
}
 
int&& func1()
{
    return func2();
}
 
int main()
{
    cout <<func1() << endl;
    return 0;
}

Почему вылазиит данный warning? Если обернуть return func2(); функцией std::move(), то все ОК.
Я к тому, что возвращенное func2() значение уже является rvalue, зачем его еще кастовать к T&& ?



0



2062 / 618 / 41

Регистрация: 23.10.2011

Сообщений: 4,468

Записей в блоге: 2

02.11.2013, 23:56

2

gromo, у меня ваш пример вообще не компилируется.

Добавлено через 1 минуту

Bash

ошибка: expected unqualified-id before «&&» token



0



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

02.11.2013, 23:56

 [ТС]

3

Цитата
Сообщение от programina
Посмотреть сообщение

у меня ваш пример вообще не компилируется.

чем компилируете? Здесь С++11



0



2062 / 618 / 41

Регистрация: 23.10.2011

Сообщений: 4,468

Записей в блоге: 2

02.11.2013, 23:58

4

Bash

ошибка: invalid initialization of non-const reference of type «int&» from an rvalue of type «int»

Добавлено через 1 минуту
Теперь компилируется



0



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 00:06

 [ТС]

5

Цитата
Сообщение от programina
Посмотреть сообщение

Теперь компилируется

и что, без варнинга?



0



2062 / 618 / 41

Регистрация: 23.10.2011

Сообщений: 4,468

Записей в блоге: 2

03.11.2013, 00:10

6

gromo, c варнингом.

Цитата
Сообщение от Википедия

По стандарту C++ временный объект, появившийся в результате вычисления выражения, можно передавать в функции, но только по константной ссылке (const &). Функция не в состоянии определить, можно ли рассматривать переданный объект как временный и допускающий модификацию (константный объект, который тоже может быть передан по такой ссылке, нельзя модифицировать (легально)). Это не проблема для простейших структур наподобие complex, но для сложных типов, требующих выделения-освобождения памяти, уничтожение временного объекта и создание постоянного может отнимать много времени, в то время как можно было бы просто перенаправить указатели.



0



Игогошка!

1801 / 708 / 44

Регистрация: 19.08.2012

Сообщений: 1,367

03.11.2013, 00:14

7

Цитата
Сообщение от gromo
Посмотреть сообщение

Почему вылазиит данный warning? Если обернуть return func2(); функцией std::move(), то все ОК.
Я к тому, что возвращенное func2() значение уже является rvalue, зачем его еще кастовать к T&& ?

Вылазит потому что rvalue reference — эта та же ссылка, но только на rvalue. А возвращать ссылку на локальный объект функции — это создавать баг.
std::move роли не играет, так как тут он ничего не делает. А при нем варнинг исчезает только из-за усложнения кода для анализа (мало ли что может вернуть std::move).



1



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 00:34

 [ТС]

8

Цитата
Сообщение от programina
Посмотреть сообщение

gromo, c варнингом.
Originally Posted by Википедия
По стандарту C++ временный объект, появившийся в результате вычисления выражения, можно передавать в функции, но только по константной ссылке (const &). Функция не в состоянии определить, можно ли рассматривать переданный объект как временный и допускающий модификацию (константный объект, который тоже может быть передан по такой ссылке, нельзя модифицировать (легально)). Это не проблема для простейших структур наподобие complex, но для сложных типов, требующих выделения-освобождения памяти, уничтожение временного объекта и создание постоянного может отнимать много времени, в то время как можно было бы просто перенаправить указатели.

RValue-reference, слышали такое?

Добавлено через 12 минут

Цитата
Сообщение от ct0r
Посмотреть сообщение

Вылазит потому что rvalue reference — эта та же ссылка, но только на rvalue. А возвращать ссылку на локальный объект функции — это создавать баг.
std::move роли не играет, так как тут он ничего не делает. А при нем варнинг исчезает только из-за усложнения кода для анализа (мало ли что может вернуть std::move).

From cplusplus.com:

Generally, rvalues are values whose address cannot be obtained by dereferencing them, either because they are literals or because they are temporary in nature (such as values returned by functions or explicit constructor calls). By passing an object to this function, an rvalue that refers to it is obtained.

Source: http://www.cplusplus.com/refer… e/?kw=move

Ну и на всякий случай:
ISO/IEC 14882:2011
Draft: n3690

Миниатюры

GCC returning reference to temporary
 



0



Игогошка!

1801 / 708 / 44

Регистрация: 19.08.2012

Сообщений: 1,367

03.11.2013, 00:38

9

Цитата
Сообщение от gromo
Посмотреть сообщение

Generally, rvalues are values whose address cannot be obtained by dereferencing them, either because they are literals or because they are temporary in nature (such as values returned by functions or explicit constructor calls). By passing an object to this function, an rvalue that refers to it is obtained.

Можешь объяснить, к чему ты это привел? Тут написано, что такое rvalues и что move делает из rvalue rvalue-reference, которая тоже является rvalue, поскольку не имеет имени. Но это я и так знаю.



0



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 00:51

 [ТС]

10

Цитата
Сообщение от ct0r
Посмотреть сообщение

move делает из rvalue rvalue-reference, которая тоже является rvalue, поскольку не имеет имени.

Зачем «делать» из rvalue rvalue-reference? По-моему компилятор не должен выводить предупреждение в 10 строке.
std::move() делает из lvalue rvalue-reference. Поэтому и возник вопрос — почему если обернуть и так временный объект, возвращаемый func2() в move(), то варнинг исчезает.
Может я в чем-то не прав, поправьте, пожалуйста.



0



Игогошка!

1801 / 708 / 44

Регистрация: 19.08.2012

Сообщений: 1,367

03.11.2013, 01:11

11

Ты в move передаешь что? rvalue. Что делает move? Грубо говоря, он делает static_cast<int&&>. У нас получается rvalue reference, у которого нет имени. Значит это rvalue. Ты возвращаешь ссылку на rvalue — на временный объект, который будет уничтожен. Это баг.



0



gromo

382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 01:28

 [ТС]

12

Цитата
Сообщение от ct0r
Посмотреть сообщение

Ты возвращаешь ссылку на rvalue — на временный объект, который будет уничтожен. Это баг.

Хорошо, а что если так:

C++ (Qt)
1
2
3
4
5
6
7
// func1() func2() как и раньше
int main()
{
    int && sss = func1();
    cout << sss << endl;
    return 0;
}

здесь мы быстренько «подхватили» этот временный, впоследствии разрушаемый временный объект, возвращаемый func1().
Я вижу, что вы выше писали, что rvalue-reference это та же ссылка, только на rvalue. Но она же предназначена как раз таки «давать имя» временным объектам. Ей даже можно присвоить литерал, и потом получить его адрес (адрес ЛИТЕРАЛА).

C++ (Qt)
1
2
3
4
5
6
int main()
{
    int && literal = 25;
    cout << &literal <<endl;
    return 0;
}

Все сказанное в этом посте не касается функции std::move

Миниатюры

GCC returning reference to temporary
 



0



Игогошка!

1801 / 708 / 44

Регистрация: 19.08.2012

Сообщений: 1,367

03.11.2013, 12:01

13

Цитата
Сообщение от gromo
Посмотреть сообщение

Хорошо, а что если так

А ничего не изменилось. Чтобы все работало правильно, надо из func1 возвращать значение. Возврат rvalue reference — это редко когда нужно.

Цитата
Сообщение от gromo
Посмотреть сообщение

здесь мы быстренько «подхватили» этот временный, впоследствии разрушаемый временный объект, возвращаемый func1()

По сути мы подхватили ссылку в никуда. rvalue reference — это не магия, она не может спасти локальный объект от уничтожения.

Цитата
Сообщение от gromo
Посмотреть сообщение

Ей даже можно присвоить литерал, и потом получить его адрес (адрес ЛИТЕРАЛА).

Можно, потому что она тут lvalue, а литерал prvalue. И никаких локальных объектов, уничтожающихся при выходе из области, тут нет, — время жизни ссылки здесь ровно такое же, как и объекта.



1



Эксперт С++

4978 / 3085 / 456

Регистрация: 10.11.2010

Сообщений: 11,164

Записей в блоге: 10

03.11.2013, 12:33

14

Цитата
Сообщение от gromo
Посмотреть сообщение

Почему вылазиит данный warning?

А где данный warning?



0



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 13:11

 [ТС]

15

Цитата
Сообщение от ct0r
Посмотреть сообщение

Возврат rvalue reference — это редко когда нужно.

С большим трудом, до меня дошло, спасибо Но что значит вот это (со стандарта):

An xvalue (an “eXpiring” value) also refers to an object, usually near the end of its lifetime (so that its
resources may be moved, for example
). An xvalue is the result of certain kinds of expressions involving
rvalue references (8.3.2). [ Example: The result of calling a function whose return type is an rvalue
reference is an xvalue.
— end example ]

Цитата
Сообщение от ct0r
Посмотреть сообщение

std::move роли не играет, так как тут он ничего не делает. А при нем варнинг исчезает только из-за усложнения кода для анализа (мало ли что может вернуть std::move).

а если вместо move поставить, например, std::abs(), то варнинг не исчезает.

И что в случае возврата больших значений из функции? Мне, например, нужно часто возвращать из функции объекты типа string размером в несколько сотен мебибайт. Не лучше ли их переместить как XValue вместо возврата по значению? Иногда такую оптимизацию делает сам компилятор, когда может.

Добавлено через 1 минуту

Цитата
Сообщение от castaway
Посмотреть сообщение

А где данный warning?

В 10 строке: returning reference to temporary [-Wreturn-local-addr]
return func2();
^

он еще в названии темы написан



0



~ Эврика! ~

1256 / 1005 / 74

Регистрация: 24.07.2012

Сообщений: 2,002

03.11.2013, 13:35

16

Цитата
Сообщение от gromo
Посмотреть сообщение

И что в случае возврата больших значений из функции? Мне, например, нужно часто возвращать из функции объекты типа string размером в несколько сотен мебибайт.

И они, конечно же, создаются на стеке. Для этого уже давно есть out-параметры и смарт-поинтеры, куда можно засунуть указатель на эту строку. Ну или move-конструктор, если строка — это единственное, что надо вернуть.

Rvalue reference как и временные объекты живут до окончания ближайшего вычисляемого выражения. Их нельзя сохранить куда-то самих по себе, чтобы они выжили до следующего выражения.

Не по теме:

Цитата
Сообщение от gromo
Посмотреть сообщение

мебибайт

Мибиби.



2



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 13:48

 [ТС]

17

Цитата
Сообщение от OhMyGodSoLong
Посмотреть сообщение

Ну или move-конструктор, если строка — это единственное, что надо вернуть.

То есть, получается, для строк и типов, которые внутри хранят указатели на дин.память move-semantics оправдана, а для фундаментальных типов уже не работает, потому что они разрушатся?



0



Игогошка!

1801 / 708 / 44

Регистрация: 19.08.2012

Сообщений: 1,367

03.11.2013, 13:57

18

Цитата
Сообщение от gromo
Посмотреть сообщение

С большим трудом, до меня дошло, спасибо

Если хочешь до конца разобраться с rvalue reference, посмотри еще universal reference и perfect forwarding.

Цитата
Сообщение от gromo
Посмотреть сообщение

Но что значит вот это (со стандарта)

Это значит, что безымянная rvalue reference — это xvalue. И что может быть перемещение ресурсов, если возвращать, например, std::move(член класса);

Цитата
Сообщение от gromo
Посмотреть сообщение

а если вместо move поставить, например, std::abs(), то варнинг не исчезает.

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

Цитата
Сообщение от gromo
Посмотреть сообщение

И что в случае возврата больших значений из функции? Мне, например, нужно часто возвращать из функции объекты типа string размером в несколько сотен мебибайт. Не лучше ли их переместить как XValue вместо возврата по значению? Иногда такую оптимизацию делает сам компилятор, когда может.

Если не пользоваться динамической памятью, то как вариант стоит посмотреть на RVO и copy elision.

Цитата
Сообщение от gromo
Посмотреть сообщение

То есть, получается, для строк и типов, которые внутри хранят указатели на дин.память move-semantics оправдана, а для фундаментальных типов уже не работает, потому что они разрушатся?

Семантика перемещения оправдана, когда нужно уметь красть ресурсы или когда объект может быть только перемещаемым. Не совсем понял про разрушение фундаментальных типов.



1



382 / 280 / 31

Регистрация: 04.09.2009

Сообщений: 1,225

03.11.2013, 15:08

 [ТС]

19

Цитата
Сообщение от ct0r
Посмотреть сообщение

Не совсем понял про разрушение фундаментальных типов.

А это я заработался немножко бред написал



0



~ Эврика! ~

1256 / 1005 / 74

Регистрация: 24.07.2012

Сообщений: 2,002

03.11.2013, 15:23

20

Цитата
Сообщение от gromo
Посмотреть сообщение

То есть, получается, для строк и типов, которые внутри хранят указатели на дин.память move-semantics оправдана, а для фундаментальных типов уже не работает, потому что они разрушатся?

Move-семантика не работает для фундаментальных типов, потому что это типы-значения. Не типы-объекты. Вы никак не можете отличить один (int) 42 от другого (int) 42. И даже от (long) 42 не можете. И это никак не исправить. Поэтому переменным фундаментальных типов выдан такой особый статус.



1



Прочитав эту статью вы узнаете:

  1. Способы, которыми можно продлить время жизни временного объекта в С++.

  2. Рекомендации и подводные камни этого механизма, с которыми может столкнуться С++ программист, и с которыми сталкивался на работе я.

Информация из статьи может быть полезна как новичкам, так и профессионалам.

Если заинтересовало, то самое время налить чая, и погнали разбираться где тут референсы висят.

Оглавление

  • Они на деревьях, Джонни!

  • 1. Способы продления времени жизни и сохранения временных значений

  • 1.1 Константная lvalue ссылка

  • 1.2 rvalue ссылка

  • 1.3 Сохранение по значению

  • 2. Выведение типов компилятором (type deduction)

  • 2.1 auto

  • 2.2 decltype

  • 2.3 decltype(auto)

  • 2.4 template

  • 3. Рекомендации и подводные камни

  • 3.1 Прежде чем объявить ссылку на другую ссылку убедитесь, что последняя не указывает на временный объект

  • 3.2 Не используйте std::move там где может использоваться NRVO

  • 3.3 Прежде чем продлевать время жизни убедитесь, что значение не является xvalue (Xray&&)

  • 3.4 При создании RAII объекта всегда сохраняйте его значение в переменную, либо же объявляйте ссылку на него

  • 3.5 Прежде чем объявить ссылку на другую ссылку убедитесь, что последняя не указывает на временный объект

  • 3.6 Не продлевайте жизнь через тернарный оператор ?:

  • 3.7 Не используйте ссылки в полях классов (особенно если они указывают на временные объекты) и не используйте std::reference_wrapper для продления жизни

  • 3.8 При передаче в new временных значений, убедитесь, что ни одно из них не сохраняется по ссылке

  • 3.9 Не продлевайте время жизни временного массива через ссылку на его элементы

  • Заключение

Они на деревьях, Джонни!

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

  1. Висячие ссылки.

  2. Всё.

Возможно в вашем варианте подобного списка будет больше пунктов, но вряд-ли они смогут потягаться с первым номером по частоте, с которой вы можете встретить их в продакшене.

Для тех кто не знает, висячая ссылка (dangling reference) — это ссылка на область памяти, в которой нет «живого» объекта. Это возможно когда время жизни ссылки дольше чем время жизни объекта, на который она указывает. Ссылка становится висячей в тот момент, когда компилятор разрушает объект (вызывает деструктор объекта и потом освобождает память, которая объектом занималась), а ссылку ещё нет.

Скрытая правда: как на самом деле появляются висячие ссылки

Скрытая правда: как на самом деле появляются висячие ссылки

Опасность висячей ссылки в том, что если кто-нибудь в это время воспользуется данной ссылкой, то может произойти всё что угодно, что описывается стандартом как неопределённое поведение или UB.

Что значит неопределённое? Если оно произойдёт то может случиться коллапс?

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

  1. Реализованного в программе алгоритма.

  2. Используемого компилятора.

    1. В частности, от поддерживаемых компилятором оптимизаций.

  3. Выбранного уровня оптимизации.

  4. Имплементации стандартной библиотеки.

  5. Операционной системы.

    И так далее…

Поэтому, теоретически, мы можем предполагать что произойдёт в конкретном случае (может ничего плохого, а может и упасть программа).

Хоть теоретически мы и можем предполагать что произойдёт в том или ином случае, провоцирующем UB, но что бы вы выбрали: расследовать убийство, или не допускать его? Я считаю, что намного важнее — научиться избегать UB в своих программах, потому что программа с неопределённым поведением — горе для заказчика, а горе заказчика — наше горе.

Один из спонсоров появления висячих ссылок — нюансы механизма продления времени жизни временных объектов. Данный механизм существует с C++03 (продление через константные lvalue ссылки). В C++11 его доработали (добавили rvalue ссылки) и он стал звучать так:

Если вы приняли временный объект по константной lvalue ссылке или rvalue ссылкe, то его время жизни будет продлено до времени жизни ссылки.

Исходя из этого утверждения можно заключить, что продление жизни работает только для временных объектов, на которые указывают ссылки. А ещё, если прибавить к этому определению сохранение по значению, то получается следующий список:

  1. Создание константной lvalue ссылки на временное значение.

  2. Создание rvalue ссылки на временное значение.

  3. Сохранение по значению.

Хотя, с точки зрения языка, сохранение по значению — не механизм продления жизни, поскольку временное значение в таком случае должно скопироваться или переместится (время жизни оригинального временного значения при этом не продлевается). Но, забегая вперёд:
1. Оптимизация copy elision приводит ко внешне похожим эффектам, как при сохранении по ссылке.
2. Сохранение по значению тоже имеет нюансы, связанные с продлением времени жизни через ссылки.

Давайте теперь поговорим про каждый из этих вариантов поподробнее. Но сперва рассмотрим класс Xray, при помощи которого мы будем отслеживать порядок вызова конструкторов и деструкторов.

Класс Xray

struct Xray
{
  Xray(std::string value)
    : mValue(std::move(value))
  {
      std::cout << "Xray ctor, value is " << mValue << std::endl;
  }
  
  Xray(Xray&& other)
    : mValue(std::move(other.mValue))
  {
      std::cout << "Xray&& ctor, value is " << mValue << std::endl;
  }
  
  Xray(const Xray& other)
    : mValue(other.mValue)
  {
      std::cout << "Xray const& ctor, value is " << mValue << std::endl;
  }
  
  ~Xray()
  {
      std::cout << "~Xray dtor, value is " << mValue << std::endl;
  }
  
  std::string mValue;
};

1. Способы продления времени жизни и сохранения временных значений

Для разогрева начнём с самого простого варианта: ситуации, в которой мы вообще не сохраняем временное значение, а только создаем его.

Если мы так сделаем, то компилятор вызовет деструктор в той же строке. Пример на godbolt:

void main()
{
  // Вывод: Xray ctor, value is 1
  // Вывод: Xray dtor, value is 1
  Xray{"1"};
  
  std::cout << "Wait a sec" << std::endl;
}

1.1 Константная lvalue ссылка

Что такое lvalue ссылка?

Левосторонние ссылки (lvalue references) — ссылки вида: Xray& и const Xray& (тип не обязательно должен быть Xray, можно и любой другой, например int). Левосторонняя ссылка всегда ссылается только на именованные, в некотором смысле постоянные значения (более подробно разберём этот вопрос в сравнении с rvalue ссылками).

Понять что ссылка левосторонняя можно по её типу (const T&, T&), либо же так: значения на которые она ссылается всегда находятся слева от = при объявлении переменной:

int i = 3;
int& iRef = i;

Здесь i — lvalue, поэтому и ссылка на него iRef называется lvalue reference.

Теперь же рассмотрим вариант с созданием константной lvalue ссылки на временный объект:

void main()
{
  // Вывод: Xray ctor, value is 1
  const Xray& xrayRef = Xray{"1"}; 
  
  // Вывод: xrayRef value is 1
  std::cout << "xrayRef value is " << xrayRef.mValue << std::endl;
  
} // Вывод: Xray dtor, value is 1

Всё чисто и просто: мы создаём временное значение при помощи Xray{"1"} и сохраняем константную ссылку на него в xrayRef. После чего временное значение разрушается при выходе из функции main (после достижения потоком исполнения программы конца тела функции — фигурной скобки } ).

Аналогично работает и следующий пример, за исключением того, что теперь при создании временного объекта произойдёт неявное преобразование типа из std::string в Xray:

void main()
{
  // Вывод: Xray ctor, value is 1
  const Xray& xrayRef = std::string("1"); 
} // Вывод: Xray dtor, value is 1

Почему компиляция этого примера не завершается ошибкой?

Данное преобразование не является ошибочным потому что в Xray объявлен конструктор, принимающийt std::string, и по умолчанию все конструкторы разрешают неявные преобразования.

При желании мы можем запретить неявное приведение типов, если пометим конструктор Xray(const std::string&) как explicit, в таком случае нам нужно будет явно вызывать конструктор Xray{std::string("1")}.

Плюс такого подхода — нет лишних вызовов конструкторов копирования. Но в этом плане это не единственный способ, обладающий таким плюсом.

1.2 rvalue ссылка

Что означает правосторонняя ссылка и как она отличается от левосторонней?

Правосторонняя ссылка, это ссылка вида: Xray&& (тип не обязательно должен быть Xray, можно и любой другой, например int). Данный вид ссылки может указывать только на временные значения. Правосторонней же она называется потому что временные значения, на которые она указывает, всегда находятся справа от= при объявлении переменной:

int&& iRvalueRef = 3;

Здесь (int)3 — rvalue (временное значение), поэтому и ссылка на него iRvalueRef называется rvalue reference.

Отличия между rvalue и lvalue

Чтобы разобраться в отличиях, давайте рассмотрим пример:

int three = 3; 
int& threeLvalueRef = three;
int&& fourRvalueRef = 4;

Здесь:
1. (int)3 и (int)4 — rvalue, временные значения. У данных значений нет имени, по которому к ним можно обратиться.
2. int three — lvalue. У неё есть имя ‘three’, по которому к ней можно обратиться.
3. int& threeLvalueRef — lvalue reference. Она ссылается на lvalue three.
4. int&& fourRvalueRef — rvalue reference. Она ссылается на rvalue (int)4.

Более подробно про виды ссылок и их различия также можно прочитать в статьях:

  1. [RU] @rhaport Понимание lvalue и rvalue в C и С++

  2. [ENG] fluentcpp Understanding lvalues, rvalues and their references

Что такое семантика перемещения и как с ней связан std::move?

rvalue reference так же можно принять переместив объект (если в классе такого типа реализован конструктор перемещения, как в Xray::Xray(Xray&&)). Для этого нужно вызвать std::move:

Xray xray = Xray{‘123”};
Xray&& xrayRef = std::move(xray);

Сам std::move не занимается какой-то магией, он только приводит тип аргумента Xray к типу правосторонней ссылки Xray&&, чтобы таким образом, вызвался конструктор с параметром Xray&&, который называется конструктором перемещения, и в котором программист должен описать логику: какие поля класса нужно переместить и как.
А нужно это потому что существуют тяжеловесные типы, значения которых лучше перемещать, чем копировать.
Пример: скопировать значение строки Xray::mValue из одного места в другое — не всегда лучший выбор, поскольку это подразумевает:

  1. Выделение, обычно, не маленького куска памяти размером mValue.size() байт.

  2. Побайтовое копирование значения каждого байта.

Такое копирование может быть очень долгим, ведь строка может быть длинной и в 10000 символов (и больше). Поэтому переместить её значение будет намного быстрее, в таком случае указатель на данные (const char*), хранящийся под капотом std::string, просто будет отдан другому экземпляру std::string, без всяких дополнительных аллокаций памяти и копирования значений байт.

Упрощённая реализация std::move для lvalue значений:

template<typename T>
T&& move(T& value) 
{
	return (T&&)value;
}

rvalue ссылка тоже подходит, если нужно ссылаться на временное значение, при этом не будет вызвано никаких дополнительных конструкторов перемещения или копирования:

void main()
{
  // Вывод: Xray ctor, value is 1
  Xray&& xrayRef = Xray{"1"};
} // Вывод: Xray dtor, value is 1

1.3 Сохранение по значению

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

void main()
{
  // Вывод: Xray ctor, value is 1
  Xray xray = Xray{"1"}; 
} // Вывод: Xray dtor, value is 1

Возможно, глядя на этот пример у вас возникает вопрос: «Разве не будет вызова копирующего конструктора?».

Дело в том, что благодаря оптимизации copy elision, предотвращающей избыточное копирование, конструктор копирования/перемещения вызван не будет. И более того, даже в таком виде, будет вызван всего 1 конструктор (который создает объект Xray):

void main()
{
  // Вывод: Xray ctor, value is 1
  Xray xray = Xray{Xray{Xray{"1"}}}; 
} // Вывод: Xray dtor, value is 1

В С++ оптимизация copy elision появилась начиная с С++98, но поддерживалась не во всех компиляторах. Когда пришёл С++17, он навел порядок, и, начиная с него, все компиляторы обязаны поддерживать эту оптимизацию.

2. Выведение типов компилятором (type deduction)

В С++ существуют механизмы, которые позволяют нам самим не определять тип переменной, или определять его только частично. В таком случае компилятор, если это возможно, выведет тип самостоятельно, на основе инициализатора (присваиваемого выражения).

Хоть механизмы выведения типа отвечают только за способ выведения типа, и в конце концов выводят тип как ссылку или безссылочный тип (это означает, что при их использовании мы в конце концов всё равно приходим к одному из способов из п.1: либо к передаче по ссылке, либо к передаче по значению). Но для полноты картины их стоит рассмотреть, хотя бы вкратце.

Далее, для проверки того какой же именно компилятор вывел тип, я буду использовать бесплатный сервис — cppinsights.

2.1 auto

Начиная с С++11 у нас появилась возможность использовать ключевое слово auto для выведения типа переменной через её инициализатор. Если мы используем его без дополнительных квалификаторов и & (as is), то при выведении типа переменной будут проигнорированы ссылочность и квалификаторы const, volatile. Это означает, что произойдёт сохранение по значению. Смотрите вывод типов на cppinsights:

void main()
{
  // Вывод: Xray ctor, value is 1
  auto xray = Xray{"1"}; // type = Xray
} // Вывод: Xray dtor, value is 1

Примеры отбрасывания квалификаторов при выведении через auto

Смотрите вывод типов на cppinsights:

int i = 0;
int& iRef = i;
const int& iConstRef = i;
volatile int& iVolatileRef = i;
const volatile int& iCVRef = i;
int* iPtr = &i;

auto _1 = i;            // type = int
auto _2 = iRef;         // type = int
auto _3 = iConstRef;    // type = int
auto _4 = iVolatileRef; // type = int
auto _5 = iCVRef;       // type = int
auto _6 = iPtr;         // type = int*

Чтобы добавить квалификаторы и ссылочность (или сохранить их при выводе типа) нужно указать их рядом с auto. При их добавлении в данном случае произойдет продление жизни через константную lvalue ссылку (cppinsights):

void main()
{
  // Вывод: Xray ctor, value is 1
  const auto& xray = Xray{"1"}; // type = const Xray&
} // Вывод: Xray dtor, value is 1

Также есть возможность указать auto&&, тогда механизм вывода будет очень похож на perfect forwarding. При его использовании в данном случае произойдет продление жизни через rvalue ссылку (cppinsights):

void main()
{
  // Вывод: Xray ctor, value is 1
  auto&& xray = Xray{"1"}; // type = Xray&&
} // Вывод: Xray dtor, value is 1

Чтобы узнать что такое perfect forwarding смотрите п.2.4 template.

Примеры вывода типа через auto&&

Смотрите вывод типов на cppinsights:

int i = 0;
const int& iConstRef = i;

auto&& _ = i;           // type = int&
auto&& _1 = iConstRef;  // type = const int&
auto&& _2 = 4;          // type = int&&

2.2 decltype

С++11 принёс нам ключевое слово decltype, которое позволяет получить тип переданного ему выражения в compile time. Примеры вывода типов с использованием decltype на cppinsights:

int i = 0;
const int& iConstRef = i;
int&& iRvalueRef = 1;

decltype(i) _1 = i;                              // type = int
decltype(iConstRef ) _2 = iConstRef;             // type = const int&
decltype(iRvalueRef) _3 = std::move(iRvalueRef); // type = int&&
decltype(3) _4 = 3;                              // type = int

При его использовании в данном случае произойдет сохранение по значению:

void main()
{
  // Вывод: Xray ctor, value is 1
  decltype(Xray{"1"}) xray = Xray{"1"}; // type = Xray
} // Вывод: Xray dtor, value is 1

Если вам при этом нужно вывести тип объекта, у которого нет нужного конструктора(в частности конструктора по умолчанию), то можно воспользоваться std::declval:

decltype(std::declval<Xray>()) xray = Xray{"1"}; // type = Xray&&

То, что вычисление типа происходит в compile time означает, что переданное в decltype выражение вычисляется не во время исполнения программы, а во время компиляции. Компилятор только смотрит на то какой тип получается в результате выражения и подставляет его.
Пример: В выраженииdecltype(2+2) не будет вычисляться результат сложения 2+2, компилятор будет рассматривать это выражение только с точки зрения типов: (int)+(int), результат — int.

При этом вы вероятно заметили, что использовать decltype в таком виде неудобно:
1. Появляется много лишней информации, которая полностью или частично дублирует присваиваемое выражение.
2. Иногда приходится использовать воркэраунды(вроде std::declval) чтобы вывести тип.

Видимо по этим причинам в следующем стандарте этот механизм доработали и выдали нам decltype(auto).

2.3 decltype(auto)

Начиная с С++14 у нас появилась возможность передавать как параметр в decltype  ключевое слово auto. decltype(auto) позволяет вывести ровно такой-же тип, как у присваиваемого выражения, то есть ссылочность и квалификаторы при таком выводе будут сохранены.
Примеры вывода типов с использованием decltype(auto) на cppinsights:

int i = 2;
const int& iConstRef = 0;

decltype(auto) _1 = 1;            // type = int
decltype(auto) _2 = iConstRef;    // type = const int&
decltype(auto) _3 = std::move(i); // type = int&&

При его использовании в данном случае произойдёт сохранение по значению (cppinsights):

void main()
{
  // Вывод: Xray ctor, value is 1
  decltype(auto) xray = Xray{"1"}; // type = Xray
} // Вывод: Xray dtor, value is 1

2.4 template

Выведение типа через шаблон очень похоже на выведение типа через auto, и наоборот.

Если мы укажем тип шаблона T (из template<typename T>) без дополнительных квалификаторов и & (as is), то при выведении типа шаблонного аргумента функции будут проигнорированы ссылочность и квалификаторы const, volatile. Это означает, что произойдёт сохранение по значению (cppinsights в данном случае показывает все типы, с которыми был инстанцирован шаблон):

template<typename T> 
void foo(T param)
{}

void main()
{
  // Вывод: Xray ctor, value is 1
  foo(Xray{"1"}); // type = Xray
} // Вывод: Xray dtor, value is 1

Примеры отбрасывания квалификаторов при выведении через шаблон

Смотрите вывод типов на cppinsights:

template<typename T> 
void foo(T param)
{}

int i = 0;
int& iRef = i;
const int& iConstRef = i;
volatile int& iVolatileRef = i;
const volatile int& iCVRef = i;
int* iPtr = &i;

foo(i);            // type = int
foo(iRef);         // type = int
foo(iConstRef);    // type = int
foo(iVolatileRef); // type = int
foo(iCVRef);       // type = int
foo(iPtr);         // type = int*
template<typename T> 
void foo(const T& param)
{}

void main()
{
  // Вывод: Xray ctor, value is 1
  foo(Xray{"1"}); // type = const Xray&
} // Вывод: Xray dtor, value is 1
template<typename T> 
void foo(T&& param)
{}

void main()
{
  // Вывод: Xray ctor, value is 1
  foo(Xray{"1"}); // type = Xray&&
} // Вывод: Xray dtor, value is 1

Примеры вывода типа через T&&

Смотрите вывод типов на cppinsights:

template<typename T> 
void foo(T&& param)
{}

int i = 0;
const int& iConstRef = i;

foo(i);            // type = int&
foo(4);            // type = int&&
foo(std::move(i)); // type = int&&
foo(iConstRef);    // type = const int&

Более подробное рассмотрение шаблонов и их отличий от других способов выведения типов выходит из рамок данной статьи, но вы можете ознакомиться с некоторыми статьями:

  1. @semenyakinVS Просто о шаблонах

  2. @4eyes О шаблонах С++, чуть сложнее


3. Рекомендации и подводные камни

3.1 Прежде чем объявить ссылку на другую ссылку убедитесь, что последняя не указывает на временный объект

Возврат ссылки из функции не продлевает время жизни, с чем связана одна из самых распространенных проблем у начинающих — они возвращают ссылку на временное значение из функции. Смотрите вывод типов на cppinsights:

const Xray& foo() 
{ 
  return Xray(“1”); 
}

// Все примеры ниже — неверные. Время жизни не будет продлено.
const Xray& _1 = foo(); // Висячая ссылка
	
auto _2 = foo(); // Тип Xray. Значение с неопределённым содержимым в Xray::mValue.
const auto& _3 = foo(); // Тип const Xray&, висячая ссылка
auto&& _4 = foo();      // Тип const Xray&, висячая ссылка
  
decltype(auto) _5 = foo();  // Тип const Xray&, висячая ссылка
decltype(foo()) _6 = foo(); // Тип const Xray&, висячая ссылка

В данном случае временный объект разрушится перед выходом из функции, поэтому возвращаемая ссылка будет висячей. В связи с этим компилятор даже выдаст предупреждение (запуск программы на godbolt):

warning: returning reference to local temporary object

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

3.2 Не используйте std::move там где может использоваться NRVO

Что такое NRVO?

NRVO (оптимизация именованного возвращаемого значения) — одна из форм copy elision, которая позволяет не копировать и не перемещать именованное значение при возврате его из функции.

В данном примере str — именованное значение (у него есть имя «str»), и если компилятор умеет делать NRVO, то не будет ни копирования ни перемещения, объект прямо поместится в value:

std::string foo() 
{
    std::string str;
    // .. изменение str
    return str; 
}

std::string value = foo();

Так же существует оптимизация RVO (оптимизация возвращаемого значения), это более простая форма той же оптимизации, когда мы не даём возвращаемому значению имя.

В данном примере std::string{"1"} — неименованное значение (у него нет имени, по сравнению с str из предыдущего примера), и если компилятор умеет делать RVO, то не будет ни копирования ни перемещения, объект прямо поместится в value:

std::string foo() 
{
    return std::string{"1"}; 
}

std::string value = foo();

Если хотите узнать больше про NRVO и RVO, то можете изучить статью @BykoIanko RVO и NRVO в C++17.

В целях оптимизации производительности иногда может возникать желание написать так:

std::string&& foo() 
{
    std::string str;
    // .. изменение str
    return std::move(str); 
}

В данном примере проблема в том, что возвращенная ссылка будет висячей, поскольку она будет указывать на локальный объект, который разрушится при выходе из функции. В связи с этим компилятор даже выдаст предупреждение, но вы можете его не заметить среди сотен других (запуск программы на godbolt):

warning: returning address of local variable or temporary: str

Если вы сильно переживаете за то что не сработает NRVO (но вероятнее всего оно сработает, почти все современные компиляторы уже умеют его делать), то лучше возвращать по значению:

std::string foo() 
{
    std::string str;
    // .. изменение str
    return std::move(str);
}

А ещё лучше — положиться на NRVO и не делать move.

3.3 Прежде чем продлевать время жизни убедитесь, что значение не является xvalue (Xray&&)

В C++03 время жизни временных объектов продлевалось при сохранении его по константной ссылке. Начиная с C++11 появились xvalue у которых время жизни объекта продлить нельзя. Разбор всех категорий значений выходит за рамки этой статьи, но чтобы говорить более предметно, я уточню: существуют несколько категорий значений вроде prvalue (то что мы до этого рассматривали как rvalue), lvalue, xvalue.

В данном случае нас интересуют xvalue. В перегрузках функций, xvalue ведут себя как rvalue, то есть xvalue будет передано в функцию как правосторонняя ссылка T&& (если такая перегрузка есть).
Пример: если у класса есть перемещающий конструктор, то будет вызван он, а не конструктор копирования (который бы был вызван в случае передачи lvalue):

Xray& lvalue();
Xray prvalue();
Xray&& xvalue();

Xray _1 = lvalue();  // Копирование Xray(Xray)
Xray _2 = prvalue(); // copy elision
Xray _3 = xvalue();  // Перемещение Xray(Xray&&)

Время жизни xvalue нельзя продлить (как и lvalue). Попытка сделать это приведёт к висячим ссылкам:

Xray const& _1 = prvalue(); // время жизни как у ссылки
Xray&& _2 = prvalue();      // время жизни как у ссылки
    
Xray& _3 = lvalue();       // висячая ссылка, не продлевает время жизни
const Xray& _4 = lvalue(); // висячая ссылка, не продлевает время жизни
Xray&& _5 = xvalue();      // висячая ссылка, не продлевает время жизни
const Xray& _6 = xvalue(); // висячая ссылка, не продлевает время жизни

3.4 При создании RAII объекта всегда сохраняйте его значение в переменную, либо же объявляйте ссылку на него

Нужно быть внимательным ко времени жизни объекта, если он является RAII оберткой. Например, не стоит объявлять std::lock_guard временным не сохранив ссылку на него (или не сохранив по значению), потому что это приведёт к преждевременному освобождению мьютекса, а значит появятся гонки:

void main()
{
  // Так можно и нужно
  std::lock_guard<std::mutex> lock{someMutex};
  
  /* Так нельзя:
     std::lock_guard<std::mutex>{mutex};
     Потому что компилятор разрушит lock_guard ещё до выхода из main,
     что приведёт к гонкам (собственно к тому, от чего мы и защищаемся мьютексом)
  */ 

  // .. многопоточно безопасные вызовы
  // .. изменение защищенных мьютексом значений
}

Если вы подумали о том, что создание такого временного объекта может быть упразднено оптимизатором, то это не так, потому что в данном случае конструктор с деструктором имеют сайд эффекты. В некоторых случаях оптимизатор может заинлайнить код конструктора и деструктора, но при этом функциональность программы останется неизменной.

3.5 Прежде чем объявить ссылку на другую ссылку убедитесь, что последняя не указывает на временный объект

Продлевать жизнь временного объекта можно лишь один раз — при первой привязке к ссылке. Схема вроде &-указывает на>&-указывает на>временный объект не продлевает время жизни повторно, а приводит к висячей ссылке и неопределённому поведению при её использовании:

template<class T> 
const T& foo(const T& in) 
{ 
  return in; 
}

const Xray& ref1 = Xray(1); // Верно, время жизни будет продлено.

Xray& ref2 = foo(Xray(2)); // Неверно, время жизни не будет продлено, 
                        // ref2 — висячая ссылка.
std::cout << ref2.mValue; // Неопределённое поведение

3.6 Не продлевайте жизнь через тернарный оператор ?:

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

Xray&& rvalRef = cond 
                ? Xray{“1”}  // Один из временных объектов
                : Xray{“2”}; // будет иметь время жизни rvalRef

const Xray& constLvalRef = cond 
                ? Xray{“1”}  // Один из временных объектов
                : Xray{“2”}; // будет иметь время жизни constLvalRef

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

3.7 Не используйте ссылки в полях классов (особенно если они указывают на временные объекты) и не используйте std::reference_wrapper для продления жизни

Во-первых, создание ссылок в полях класса на объекты, временем жизни которых класс не управляет, с большей вероятностью (по сравнению с передачей по значению) может привести к висячим ссылкам.

Во-вторых, сам этот механизм работает нестабильно. Хоть в стандарте и сказано: если временный объект имеет ссылочное поле, инициализированное другим временным объектом, то продление времени жизни рекурсивно применяется к инициализатору этого поля:

struct X 
{ 
  const int& lvalRef; 
};

const X& lvalRef = X{1}; // Временные значения X и (int)1 будут иметь время жизни lvalRef 
X&& rvalRef = X{1};      // Временные значения X и (int)1 будут иметь время жизни rvalRef 

auto&& _1 = X{1};         // Тоже ок, тип Xray&& (продление по rvalue ссылке)
decltype(auto) _2 = X{1}; // Тоже ок, тип Xray (сохранение по значению)

Но похоже, что этот трюк срабатывает только если у вас aggregate-initialization(в случае вызова X x{1}) или если компилятор поддерживает copy elision (в случае вызова X x = X{1}). Возвращаясь к нашему примеру, добавив конструктор для инициализации нашего значения, он начинает вести себя нестабильно на разных компиляторах:

struct X 
{ 
  template<typename T>
  X(T&& l)
    : val(l)
  {}
  
	const int& val;
};

const X& lvalRef = X{1}; // Висячая ссылка, значение lvalRef.val == 0
X&& rvalRef = X{1};      // Висячая ссылка, значение rvalRef .val == 0
auto&& _1 = X{1};       // Висячая ссылка, значение _1 .val == 0, тип X&& (продление по rvalue ссылке)
decltype(auto) _2 = X{1}; // Тип X (сохранение по значению),
                          // На msvc(trunk) значение _2 .val == 1, 
                          // но на gcc(trunk) это висячая ссылка, значение _2 .val == 0

Наиболее интересной выглядит часть decltype(auto) _2 = X{1}, которая компилируется в X _2 = X{1} и её результаты. Исходная формулировка, которая описывает выражения у которых расширяется время жизни несколько туманна, и по ней не до конца понятен весь список ситуаций, которые имеются ввиду:

the initializer expression is used to initialize the destination object

Но я предполагаю, что в случае decltype(auto) _2 =  X{1} время жизни должно быть продлено, потому что временный объект используется в выражении, которое является инициализатором поля X. Поэтому думаю, что то, что время жизни не продлевается — баг компилятора.

В связи с вышеописанным я не рекомендую использовать ссылки на временные значения в полях класса, поскольку этот механизм:

  1. Работает нестабильно в зависимости от компилятора.

  2. Имеет туманную формулировку, на основе которой приходится строить догадки.

3.8 При передаче в new временных значений, убедитесь, что ни одно из них не сохраняется по ссылке

Временные объекты, переданные как параметры при инициализации в new, будут жить до тех пор пока не закончится вызов new. Это означает, что не стоит сохранять ссылки на временные значения в полях класса, который создается через new, поскольку это приведет к висячим ссылкам:

struct S 
{ 
  int i; const std::pair<int,int>& pair; 
};

S a { 1, {2,3} };         // верно, хоть и работает нестабильно (см. п.3.7)
S* p = new S{ 1, {2,3} }; // неверно, p->pair — висячая ссылка

3.9 Не продлевайте время жизни временного массива через ссылку на его элементы

В С++ есть возможность продлить время жизни временного массива, создав ссылку на один из его элементов. Я рекомендую использовать этот механизм только в полемике на кухне с коллегами (и, возможно, в метапрограммировании на старых стандартах С++), поскольку он интуитивно не понятен.

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

int id = 0;
int&& a = int[2]{1, 2}[id];

К нашему счастью, код в таком виде не скомпилируется. Чтобы он заработал нужно будет создавать массив чуть менее очевидным способом (cppinsights):

template<typename T>
using dummy = T;

int main()
{
  int i = 1;
  const int& a = dummy<int[2]>{1, 2}[i]; // тип const int&
}

При этом можно так же продлить жизнь через rvalue ссылку:

int&& a = dummy<int[3]>{1, 2, 3}[i];

Хотя пример с rvalue ссылкой уже ведёт себя нестабильно на разных компиляторах:

  • На gcc всё работает как надо (godbolt).

  • А на msvc ошибка компиляции cannot convert from 'int' to 'int &&' (godbolt).

Судя по всему, это баг msvc. Но мы можем обойти его если будем принимать значение через auto&& (godbolt, cppinsights):

auto&& a = dummy<int[3]>{1, 2, 3}[i]; // тип int&&

Заключение

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

Сжальтесь над программистом, который будет читать то что вы написали, он может не знать всех этих нюансов. Да даже если он где-то про них и слышал и даже знает некоторые из них, то ему может не хватить концентрации чтобы уследить за каждым. Вероятнее всего даже самый образованный специалист, читающий ваш код, через раз всё равно будет сомневаться не висячая ли это ссылка.

Большинство данных механизмов и связанных с ними подводных камней стоит использовать только в теоретических изысканиях и операциях по обезвреживанию опасного кода.

Избегайте остроумия и HolyHandGrenade, всем KISS.

UPD 06.06:

  1. Исправил ошибку в разогревающем примере из 1. Способы продления времени жизни и сохранения временных значений. Спасибо @HungryD!

  2. Добавил поясняющий комментарий в примере с lock_guard из 3.4 При создании RAII объекта всегда сохраняйте его значение в переменную, либо же объявляйте ссылку на него. Спасибо @findoff!

UPD 07.06:

  1. Исправил ошибку в примере и добавил более подробный анализ в 3.9 Не продлевайте время жизни временного массива через ссылку на его элементы. Спасибо @Apoheliy!

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

Была ли вам полезна статья?


52.7%
Да, я узнал новые способы продления жизни временных значений
39


56.76%
Да, я узнал новые подводные камни
42


54.05%
Да, я узнал полезные рекомендации
40


1.35%
Нет, я не С++ программист
1

Проголосовали 74 пользователя.

Воздержались 18 пользователей.

Guest

  • They’re in the trees, Johnny!
  • 1. Ways to extend the lifetime and to store temporary objects
    • 1.1 Const lvalue reference
    • 1.2 rvalue reference
    • 1.3 Storing by value
  • 2. Type deduction
    • 2.1 auto
    • 2.2 decltype
    • 2.3 decltype(auto)
    • 2.4 template
  • 3. Tips and tricks
    • 3.1 Before referencing another reference, make sure that this ‘another’ reference doesn’t point out at a temporary object
    • 3.2 Do not use std::move when NRVO can be used
    • 3.3 Make sure that the value is not xvalue (Xray&&) before extending the lifetime of temporary objects
    • 3.4 Always save the value of the RAII object created into the variable, or declare a reference to it
    • 3.5 Before referencing another reference, make sure that this ‘another’ reference doesn’t point out at a temporary object
    • 3.6 Do not extend the lifetime of a temporary object via the ternary operator ?:
    • 3.7 Do not use references in class fields (if they point to temporary objects especially) and do not use std::reference_wrapper to extend the lifetime of a temporary object
    • 3.8 When passing temporary objects to new, make sure that none of them are stored by reference
    • 3.9 Do not extend the lifetime of a temporary array through a reference to its elements
  • Conclusion

After reading this article, you will learn the following: ways to extend the lifetime of a temporary object in C++, various tips and tricks; pitfalls of the lifetime extension that a C++ programmer may face, and that I have already faced myself.

1006_lifetime_extension/image1.png

This article may be useful not only for beginners but also for advanced developers.

If interested, grab a cup of tea and let’s go find out where the references are dangling.

We published and translated this article with the copyright holder’s permission. The author is Evgeny Neruchek (cheshirwr@mail.ru). The article was originally published on Habr.

They’re in the trees, Johnny!

To begin with, let’s identify a range of possible problems with references:

  • Dangling references.
  • That’s all.

You may enumerate much more problems with the references, but any other issues pale in comparison with dangling references. Dangling references are the most frequent of any production problems.

For those who do not know, a dangling reference is a reference to memory that contains an object which no longer exists. It happens, when the lifetime of an object expires before the lifetime of the reference that points to it expires. The reference becomes dangling when the compiler destroys the object (calls a destructor for the object and then releases the storage space allocated for this object), while the reference to this object still exists.

1006_lifetime_extension/image2.gif

And if someone uses this dangling reference anything can happen. The C++ standard describes this ‘anything’ as undefined behavior or UB.

What does ‘undefined’ mean? If it occurs, could everything collapse?
open icon

The behavior is called ‘undefined’ as to the C++ standard. It means that the C++ language cannot determine what exactly should happen during the program’s execution. In fact, undefined behavior depends on the program and the environment in which it runs. Thus, UB depends on:

  • the algorithm;
  • the complier; operations supported by the complier;
  • the optimization level;
  • a standard library implementation;
  • OS.

And so on…

That’s why we can only guess what would happen in any particular case (maybe nothing bad, or maybe your program would crash).

Although we still can theorize what will actually happen in case of UB, but let me draw an analogy — what’s better: to investigate a murder or to prevent it? Since any program containing undefined behavior is a potential problem for a customer, it’s also our pain in the neck. That’s why I think avoiding and preventing undefined behavior is vital.

One of the reasons for dangling references is the peculiarities of the temporary objects’ lifetime extension. You can extend the lifetime of a temporary object since C++03 (the extension is made through the const lvalue references). In C++11 the extension mechanism was modified (rvalue references were added):

If you receive a temporary object by const lvalue reference or rvalue reference, then its lifetime is extended to the lifetime of the reference.

So, the lifetime extension is only available for referenced temporary objects. And yet, if we add storing by value to the proposition above, we get the following results:

  • A const lvalue reference to the value of a temporary object.
  • An rvalue reference to the value of a temporary object.
  • Storing by value.

Although, from the C++ viewpoint, storing by value is not a specific mechanism for the lifetime extension, since the value of a temporary object must be copied or moved (the lifetime of the original temporary value is not extended). But let us go ahead:

  • Copy elision optimization leads to the externally similar effects, as when storing by reference.
  • Storing by value also has some details related to the lifetime extension through references.

We will take a closer look at each of these two options. But first let’s inspect the Xray class. The Xray class enables us to track the order of constructors’ and destructors’ calls.

Xray class
open icon

struct Xray
{
  Xray(std::string value)
    : mValue(std::move(value))
  {
      std::cout << "Xray ctor, value is " << mValue << std::endl;
  }
  
  Xray(Xray&& other)
    : mValue(std::move(other.mValue))
  {
      std::cout << "Xray&& ctor, value is " << mValue << std::endl;
  }
  
  Xray(const Xray& other)
    : mValue(other.mValue)
  {
      std::cout << "Xray const& ctor, value is " << mValue << std::endl;
  }
  
  ~Xray()
  {
      std::cout << "~Xray dtor, value is " << mValue << std::endl;
  }
  
  std::string mValue;
};

1. Ways to extend the lifetime and to store temporary objects

1006_lifetime_extension/image3.png

To warm up, let’s start with the simplest option: when we don’t store the value of a temporary object but only create it.

In this case, the compiler calls the destructor in the same line. Here’s the example on godbolt:

void main()
{
  // Output: Xray ctor, value is 1
  // Output: Xray dtor, value is 1
  Xray{"1"};
  
  std::cout << "Wait a sec" << std::endl;
}

1.1 Const lvalue reference

What is an lvalue reference?
open icon

The lvalue references are references of the Xray& or const Xray& types (but the reference doesn’t have to always be of the Xray type, it can also be of the int type, for example). Lvalue reference always points out to named (or constant) values (we will discuss it in more detail in comparison to rvalue references).

It’s easy to recognize a reference as an lvalue reference by its type (const T&, T&). Moreover, the values it refers to are on the left side of = when the variable is declared:

int i = 3;
int& iRef = i;

In this case, i refers to lvalue, that’s why iRef is called lvalue reference.

Now let’s take a look at the second option of creating a constant lvalue reference to a temporary object:

void main()
{
  // Output: Xray ctor, value is 1
  const Xray& xrayRef = Xray{"1"}; 
  
  // Output: xrayRef value is 1
  std::cout << "xrayRef value is " << xrayRef.mValue << std::endl;
  
} // Output: Xray dtor, value is 1

Everything is simple: we create a value of a temporary object through Xray{«1»}  and then store the const reference to this object in xrayRef. After that, a value of a temporary object gets destroyed when the main function is exited (after the program’s execution flow encounters the end of the function body — the brace } ).

The following case is similar, but here the implicit type conversion from std::string to Xray occurs when creating a temporary object:

void main()
{
  // Output: Xray ctor, value is 1
  const Xray& xrayRef = std::string("1"); 
} // Output: Xray dtor, value is 1

Why does the compilation is successful in this case?
open icon

The reason for the successful compilation is that the constructor, which takes the t std::string, is declared in Xray. All constructors support implicit conversion by default.

If necessary, we can block implicit type conversion by marking the Xray(const std::string&) constructor as explicit. Then we should call the Xray{std::string(«1»)} constructor explicitly.

The main advantage of this approach is that there are no extra copy constructor calls. But this approach is not the only one that has this advantage.

1.2 rvalue reference

What does rvalue mean, and how does it differ from a lvalue?
open icon

The rvalue reference is a reference of the Xray&& type (but the reference doesn’t have to always be the Xray type, it can also be the int type, for example). The rvalue reference points only at temporary objects. The rvalue is so called because it can be used only on the righthand side of an assignment.

int&& iRvalueRef = 3;

Here (int)3 refers to rvalue (the value of a temporary object), that’s why iRvalueRef is called lvalue reference.

Difference between rvalue and lvalue

Let’s look at the example below to make the differences clear:

int three = 3; 
int& threeLvalueRef = three;
int&& fourRvalueRef = 4;

In this case:

1. (int)3 and (int)4 are rvalue, temporary objects. These temporary objects don’t have any names to be called.

2. int three is lvalue. The lvalue reference has the name ‘three’ by which it can be called.

3. int& threeLvalueRef is an lvalue reference. It references to lvalue three.

4. int&& fourRvalueRef is an rvalue reference. It references rvalue (int)4.

Read this article to learn more about reference types and their differences: Understanding lvalues, rvalues and their references [EN] (fluentcpp).

What is move semantics, and how is the std::move function related to it?
open icon

You can also recieve an rvalue reference by moving the object (if a move constructor is implemented in a class, as in Xray::Xray(Xray&&)). To do it, call the std::move function:

Xray xray = Xray{′123″};
Xray&& xrayRef = std::move(xray);

The std::move function does not make any magic, it only casts the type of the Xray parameter to the class type of rvalue reference — Xray&&. The std::move function thereby calls a constructor, whose parameter is Xray&&. This constructor is called the move constructor. Then developers need to consider the logic of the move constructor: which class fields are to be moved and in what way.

There is a reason for that. There are heavy types whose values are better to move than to copy.

Example: copying the value of the Xray::mValue string is not always the best option, since copying requires:

  • Memory allocation for mValue.size() bytes.
  • Bitwise copying.

Such copying can take a long time, since the string may easily be of 10,000 characters long (or more). That’s why moving the string value is much faster than copying it. Thus, the data pointer (const char*), that is under the hood of std::string, is passed to another std::string class, and no additional memory allocation or copying is performed.

Here’s the simplified implementation of the std::move for lvalue references:

template<typename T>
T&& move(T& value) 
{
return (T&&)value;
}

The rvalue reference is also used to point out at the value of a temporary object without any additional copy or move constructors being called.

void main()
{
  // Output: Xray ctor, value is 1
  Xray&& xrayRef = Xray{"1"};
} // Output: Xray dtor, value is 1

1.3 Storing by value

Storing by value looks as follows:

void main()
{
  // Output: Xray ctor, value is 1
  Xray xray = Xray{"1"}; 
} // Output: Xray dtor, value is 1

«Won’t the copy constructor be called?» — you may wonder looking at the case above.

But the copy elision optimization prevents redundant copying, so the copy/move constructor is not called. And moreover, even in this case only 1 constructor (which creates the Xray object) is called:

void main()
{
  // Output: Xray ctor, value is 1
  Xray xray = Xray{Xray{Xray{"1"}}}; 
} // Output: Xray dtor, value is 1

Since C++98, the copy elision optimization was introduced, but not all compliers supported it. Since C++17, all compilers are required to support the copy elision optimization.

2. Type deduction

1006_lifetime_extension/image4.png

In C++ type deduction refers to the automatic or half-automatic detection of the type of a variable. In this case, the compiler is able to deduce the type through the initializer (of the assigned expression).

The type-deduction mechanism determines only the way in which the type is deduced — as a reference or as a non-reference type. So, we still end up with one of the options from the first paragraph as a result of the type deduction: either passing by reference, or passing by value. We’re going to briefly discuss the ways of the type deduction to get the full picture.

I used cppinsights to check what type the complier deduced.

2.1 auto

Since C++11, we’re able to use the auto keyword to deduce the type of a variable through its initializer. If we use the auto keyword without any additional qualifiers and & (as is) during the type deduction of the variable type, then a reference declaration and the const and volatile type qualifiers are ignored. In this case storing by value occurs. Check the type deduction on cppinsights:

void main()
{
  // Output: Xray ctor, value is 1
  auto xray = Xray{"1"}; // type = Xray
} // Output: Xray dtor, value is 1

The examples of the qualifiers dropped during the auto type deduction
open icon

Check the type deduction on cppinsights:

int i = 0;
int& iRef = i;
const int& iConstRef = i;
volatile int& iVolatileRef = i;
const volatile int& iCVRef = i;
int* iPtr = &i;

auto _1 = i;            // type = int
auto _2 = iRef;         // type = int
auto _3 = iConstRef;    // type = int
auto _4 = iVolatileRef; // type = int
auto _5 = iCVRef;       // type = int
auto _6 = iPtr;         // type = int*

To add qualifiers and a reference declaration (or to save them during the type deduction), you need to specify them next to the auto keyword. When qualifiers and reference types are added, the lifetime is extended through a const lvalue reference (cppinsights):

void main()
{
  // Output: Xray ctor, value is 1
  const auto& xray = Xray{"1"}; // type = const Xray&
} // Output: Xray dtor, value is 1

You can also specify auto&& so the type deduction mechanism would be quite similar to perfect forwarding. When you use auto&&, the lifetime is extended through an rvalue reference (cppinsights):

void main()
{
  // Output: Xray ctor, value is 1
  auto&& xray = Xray{"1"}; // type = Xray&&
} // Output: Xray dtor, value is 1

Learn more about perfect forwarding in the 2.4 template paragraph.

The examples of the auto&& type deduction
open icon

Check the type deduction on cppinsights:

int i = 0;
const int& iConstRef = i;

auto&& _ = i;           // type = int&
auto&& _1 = iConstRef;  // type = const int&
auto&& _2 = 4;          // type = int&&

2.2 decltype

Since C++11 with the introduction of the decltype keyword, we’re able to obtain the type of an expression at compile time. The examples of the type deduction with the decltype keyword on cppinsights:

int i = 0;
const int& iConstRef = i;
int&& iRvalueRef = 1;

decltype(i) _1 = i;                              // type = int
decltype(iConstRef ) _2 = iConstRef;             // type = const int&
decltype(iRvalueRef) _3 = std::move(iRvalueRef); // type = int&&
decltype(3) _4 = 3;                              // type = int

So, if you use the decltype keyword, then storing by value occurs:

void main()
{
  // Output: Xray ctor, value is 1
  decltype(Xray{"1"}) xray = Xray{"1"}; // type = Xray
} // Output: Xray dtor, value is 1

If you need to deduce the type of an object that does not have the required constructor (in particular, the default constructor), then you can use std::declval:

decltype(std::declval<Xray>()) xray = Xray{"1"}; // type = Xray&&

The type deduction at compile time means that an expression passed to decltype is processed not during program execution, but during compilation. The compiler only checks which type is the result of the expression and then substitutes it.

Example: in the decltype(2+2) expression, the result of the addition (2+2) is not processed, since the compiler sees this expression only in terms of types: (int)+(int), so the result is int.

You may have noticed that using decltype in this way is not very convenient:

1. You get a lot of extra information that completely or partially duplicates the expression being assigned.

2. Once in a while you have to use various workarounds (such as std::declval) to deduce the type.

That’s why decltype was modified into decltype(auto) in the next standard.

2.3 decltype(auto)

Since C++14, we’re able to pass the auto keyword to decltype as a parameter. With decltype(auto) we can deduce the same type as the one of the expressions assigned, thus the qualifiers and a reference declaration are saved.

The examples of the type deduction with decltype(auto) on cppinsights:

int i = 2;
const int& iConstRef = 0;

decltype(auto) _1 = 1;            // type = int
decltype(auto) _2 = iConstRef;    // type = const int&
decltype(auto) _3 = std::move(i); // type = int&&

Thus, if you use decltype(auto), then storing by value occurs (cppinsights):

void main()
{
  // Output: Xray ctor, value is 1
  decltype(auto) xray = Xray{"1"}; // type = Xray
} // Output: Xray dtor, value is 1

2.4 template

The template type deduction is pretty similar to the auto type deduction.

If we specify the template type T (from template<typename T>) without any additional qualifiers and & (as is), during the template argument deduction in a function template, then a reference declaration and the const and volatile qualifiers are ignored. In this case storing by value occurs (on cppinsights you can see all the types instantiated with a template):

template<typename T> 
void foo(T param)
{}

void main()
{
  // Output: Xray ctor, value is 1
  foo(Xray{"1"}); // type = Xray
} // Output: Xray dtor, value is 1

The examples of the qualifiers dropped during the template type deduction
open icon

Check the type deduction on cppinsights:

template<typename T> 
void foo(T param)
{}

int i = 0;
int& iRef = i;
const int& iConstRef = i;
volatile int& iVolatileRef = i;
const volatile int& iCVRef = i;
int* iPtr = &i;

foo(i);            // type = int
foo(iRef);         // type = int
foo(iConstRef);    // type = int
foo(iVolatileRef); // type = int
foo(iCVRef);       // type = int
foo(iPtr);         // type = int*

To add qualifiers and a reference declaration (or to save them during the type deduction), you need to specify them next to the name of a template parameter.

In this case, the lifetime is extended via a const lvalue reference (cppinsights):

template<typename T> 
void foo(const T& param)
{}

void main()
{
  // Output: Xray ctor, value is 1
  foo(Xray{"1"}); // type = const Xray&
} // Output: Xray dtor, value is 1

You can also specify T&& (forwarding reference) so the type deduction and the value passing mechanisms would be quite similar to perfect forwarding.

In this case, the lifetime is extended via an rvalue reference (cppinsights):

template<typename T> 
void foo(T&& param)
{}
void main()
{
  // Output: Xray ctor, value is 1
  foo(Xray{"1"}); // type = Xray&&
} // Output: Xray dtor, value is 1

The examples of the T&& type deduction
open icon

Check the type deduction on cppinsights:

template<typename T> 
void foo(T&& param)
{}

int i = 0;
const int& iConstRef = i;

foo(i);            // type = int&
foo(4);            // type = int&&
foo(std::move(i)); // type = int&&
foo(iConstRef);    // type = const int&

Learn more about various templates in other articles:

  • Templates in simple words [RU] @semenyakinVS;
  • A few more words about templates in C++ [RU] @4eyes.

3. Tips and tricks

3.1 Before referencing another reference, make sure that this ‘another’ reference doesn’t point out at a temporary object

When you return a reference from a function you don’t extend the reference’s lifetime. This is a typical problem for beginners — they return a reference to a temporary value from a function. Check the type deduction on cppinsights:

const Xray& foo() 
{ 
  return Xray(′′1′′); 
}

// All cases below are incorrect. Lifetime is not extended.
const Xray& _1 = foo(); // Dangling reference

auto _2 = foo(); // the Xray type. Undefined value
                 // content in Xray::mValue.
const auto& _3 = foo(); // the const Xray& type, dangling reference
auto&& _4 = foo();      // the const Xray& type, dangling reference
  
decltype(auto) _5 = foo();  // the const Xray& type, dangling reference
decltype(foo()) _6 = foo(); // the const Xray& type, dangling reference

In this case the temporary object gets destroyed when the function exits, so the returning reference is dangling. As a result, the compiler issues a warning (run a program on godbolt):

warning: returning reference to local temporary object

But this warning is useless if you have thousands of them (it’s easy to miss), or if you just don’t read warnings. You can only be aware of this nuance and be attentive, or to use a sanitizer tool.

3.2 Do not use std::move when NRVO can be used

What’s NRVO?
open icon

NRVO (named return value optimization) is the variant of copy elision that allows you to avoid copying/moving a named value that is returned from a function.

In this case, str is a named value (since it has a ‘str’ name). If the compiler is able to use NRVO, an object is placed directly into value and no additional copying or moving is needed:

std::string foo() 
{
    std::string str;
    // .. str change
    return str; 
}

std::string value = foo();

There is another complier optimization — RVO (return value optimization). RVO is a more simplified optimization that also allows to avoid creating a name for return values.

In the following case, std::string{«1»} is not a named value (because, unlike str in the case above, it doesn’t have a name). If the cmpiler is able to use RVO, no copying or moving is performed, since an object is placed directly into value:

std::string foo() 
{
    return std::string{"1"}; 
}

std::string value = foo();

You can read this article to learn more about NRVO and RVO: RVO and NRVO in C++17 [RU] @BykoIanko.

To optimize performance, you may think of writing something like this:

std::string&& foo() 
{
    std::string str;
    // .. str change
    return std::move(str); 
}

In the above case, the returning reference is dangling because it points at the local object that gets destroyed when the function is exited. So, the compiler issues a warning which can easily be missed among other warnings (run a program on godbolt):

warning: returning address of local variable or temporary: str

You can return by value just in case you’re worried that NRVO would not be performed (but it would, since all modern compilers are able to perform it):

std::string foo() 
{
    std::string str;
    // .. str change
    return std::move(str);
}

But it’s always better to rely on NRVO and not use move.

3.3 Make sure that the value is not xvalue (Xray&&) before extending the lifetime of temporary objects

In C++03, the lifetime of temporary objects was extended through saving them by a constant reference. Since C++11, xvalues were introduced whose lifetime couldn’t be extended. We’re not going to analyze all value categories in detail. So, long story short, there are several value categories: prvalue (that was previously described as rvalue), lvalue and xvalue.

In this particular case we speak about xvalue. In overloads of the called functions, xvalues are quite similar to rvalues. Thus, xvalue is passed into the function as an rvalue reference T&& (if there’s an overload of the called function).

Example: the move constructor is called, if the class has one (the copy constructor is called when an lvalue reference is passed):

Xray& lvalue();
Xray prvalue();
Xray&& xvalue();

Xray _1 = lvalue();  // Xray(Xray) copying
Xray _2 = prvalue(); // copy elision
Xray _3 = xvalue();  // Xray(Xray&&) moving

You cannot extend the lifetime of xvalue (the same is true for lvalue). The lifetime extension of xvalue results in dangling references:

Xray const& _1 = prvalue(); // the lifetime of a reference
Xray&& _2 = prvalue();      // the lifetime of a reference
    
Xray& _3 = lvalue();       // dangling reference, does not extend the lifetime
const Xray& _4 = lvalue(); // dangling reference, does not extend the lifetime
Xray&& _5 = xvalue();      // dangling reference, does not extend the lifetime
const Xray& _6 = xvalue(); // dangling reference, does not extend the lifetime

3.4 Always save the value of the RAII object created into the variable, or declare a reference to it

It would be better to pay special attention to the lifetime of the RAII object. For example, don’t declare std::lock_guard as a temporary object, when you don’t save a reference to it (or don’t store the object by value). It leads to the early mutex release, which creates a race condition:

void main()
{
  // Do this
  std::lock_guard<std::mutex> lock{someMutex};
  
  /* Don't do that:
     std::lock_guard<std::mutex>{mutex};
     The complier destroys lock_guard before it leaves main,
 which creates race conditions (that we are trying to avoid with mutex)
  */ 

  // .. multi-threaded safe calls
  // .. changing mutex-protected values
}

You may have thought that the creation of a temporary object is canceled by the optimizer, but it is not. In this case, constructor and destructor have side effects. In some other cases, the optimizer may inline the constructor and destructor code, but the functionality of the program is the same.

3.5 Before referencing another reference, make sure that this ‘another’ reference doesn’t point out at a temporary object

You can extend the lifetime of a temporary object once, when binding to a reference for the first time. When you do ‘&-points to>&-points to>temporary object’, it does not extend the lifetime repeatedly, but it leads to a dangling reference and undefined behavior:

template<class T> 
const T& foo(const T& in) 
{ 
  return in; 
}

const Xray& ref1 = X(1); // True, the lifetime is extended.

Xray& ref2 = foo(X(2)); // False, the lifetime is not extended, 
                        // ref2 — dangling reference.
std::cout << ref2.mValue; // Undefined behavior

3.6 Do not extend the lifetime of a temporary object via the ternary operator ?:

If you save a reference to an expression obtained with a ternary operator, the lifetime of one of the temporary objects extends depending on the condition:

Xray&& rvalRef = cond 
                ? Xray{′′1′′}  // One of the temporary objects
                : Xray{"2"}; // will have a lifetime of rvalRef

const Xray& constLvalRef = cond 
                ? Xray{′′1′′}  // One of the temporary objects
                : Xray{"2"}; // will have a lifetime of constLvalRef

Since this mechanism is not so easy-to-use, I would not recommend you to use it in your code.

3.7 Do not use references in class fields (if they point to temporary objects especially) and do not use std::reference_wrapper to extend the lifetime of a temporary object

The first reason is that when you create references in class fields to objects whose lifetime is not controlled by the class, this is more likely to result in dangling references (compared to passing by value).

The second reason — this mechanism is unstable. Although the standard specifies that if a temporary object has a reference field initialized by another temporary object, then the lifetime extension is recursively applied to the initializer of this field:

struct X 
{ 
  const int& lvalRef; 
};

const X& lvalRef = X{1}; // Temporary objects X and (int)1
                         // will have a lifetime of lvalRef 
X&& rvalRef = X{1};      // Temporary objects X and (int)1
                         // will have a lifetime of rvalRef 

auto&& _1 = X{1};         // It's also ok, the type is Xray&&  
                          // (extension with an rvalue reference)
decltype(auto) _2 = X{1}; // It's also ok, the type is Xray 
                          // (storing by value)

But it seems like it works only if there is an aggregate-initialization (when Xray x{1}) is called), or if the compiler supports copy elision (when Xray x = Xray{1} is called). So, returning to our case, the constructor added to initialize the value is unstable on different compilers:

struct X 
{ 
  template<typename T>
  X(T&& l)
    : val(l)
  {}
  
const int& val;
};

const X& lvalRef = X{1}; // Dangling reference, the lvalRef.val value == 0
X&& rvalRef = X{1};      // Dangling reference, the rvalRef .val value == 0
auto&& _1 = X{1};        // Dangling reference, the _1 .val value == 0,
                         // the X&& type (has a lifetime of rvalue)
decltype(auto) _2 = X{1};  // X type (storing by value),
                           // On msvc(trunk) the _2 .val value == 1,
                           // but on gcc(trunk) it's dangling,  
                           // the _2 .val value == 0

The decltype(auto) _2 = X{1} part, which is compiled into X _2 = X{1}, and its results seem to be the most curious. The following description of the expressions with the lifetime extended is pretty much obscure:

the initializer expression is used to initialize the destination object

But I think that in case of decltype(auto) _2 =  Xray{1}, the lifetime is extended, since the temporary object is used in the initializer expression of the Xray field. So, I think that this ‘non-extension’ of the lifetime is a compiler bug.

That’s why I wouldn’t recommend you to reference temporary objects in class fields, because:

  • This mechanism depends on the compiler and may be unstable;
  • You can only guess the result, since the description of the mechanism is unclear.

3.8 When passing temporary objects to new, make sure that none of them are stored by reference

Temporary objects were passed as parameters when initializing to new. Thus, the lifetime of these objects lasts until the new call is executed. It means that storing references to temporary objects in class fields (that are created through new) results in dangling references:

struct S 
{ 
  int i; const std::pair<int,int>& pair; 
};

S a { 1, {2,3} };         // true, but it's unstable (see 3.7)
S* p = new S{ 1, {2,3} }; // false, p->pair — dangling reference

3.9 Do not extend the lifetime of a temporary array through a reference to its elements

You can extend the lifetime of a temporary array by referencing one of its elements. C++ has a special mechanism for this. But I would recommend you using it only in the disputes with your co-workers (and maybe in metaprogramming with the old C++ standards), since this mechanism is not so easy-to-use.

However, using this mechanism does not result in dangling references, and the lifetime of the temporary array is extended to the lifetime of the reference to its element:

int id = 0;
int&& a = int[2]{1, 2}[id];

Fortunately, the code in its current form is not compiled. You need to create an array in a less obvious way to make this code work (cppinsights):

template<typename T>
using dummy = T;

int main()
{
  int i = 1;
  const int& a = dummy<int[2]>{1, 2}[i]; // the const int& type
}

You can also extend the lifetime of a temporary object through an rvalue reference:

int&& a = dummy<int[3]>{1, 2, 3}[i];

However, the lifetime extension through an rvalue reference is sometimes unstable on different compilers:

But everything is ok on gcc (godbolt).

There is a compilation error on msvc: cannot convert from ‘int’ to ‘int &&’ (godbolt).

It’s likely to be a msvc’s bug. But we still can get around this bug, if we take the value via auto&& (godbolt, cppinsights):

auto&& a = dummy<int[3]>{1, 2, 3}[i]; // the int&& type

Conclusion

Saving the value by a constant reference is stressful for anyone who has ever faced a dangling reference. It’s important to monitor an object’s lifetime. It would be better not to put it on the compiler.

Please, have mercy on a developer who is going to read your code, they may not know all the shades of the object’s lifetime extension. And if they do know something about it, they may not be able to keep an eye on everything at once. Even the most educated computer science expert may doubt when reading your code whether a reference is dangling or not.

It would be better to use most of these mechanisms only in theoretical research or to neutralize dangerous code.

Stay away from any over-thinking stuff and HolyHandGrenade. KISS.

1006_lifetime_extension/image5.png

We can email you a selection of our best articles once a month

Подскажите, это в современных стандартах законно?

Конечно же нет. Ни в современных, ни в каких.

Метод boo отрабатывает, но вот объект разрушается раньше, тогда вопрос, почему отрабатывает метод?

А почему ему не отрабатывать?

Читал в блоге у алены си пи пи, что все ок типа, но статья старая.

Не надо читать всякую ламерню.

slovazap ★★★★★

(10.06.21 17:59:55 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от slovazap 10.06.21 17:59:55 MSK

А почему тогда память где остается реализация методов класса не очищается?

da17

(10.06.21 18:03:02 MSK)

  • Показать ответы
  • Ссылка

Метод boo отрабатывает, но вот объект разрушается раньше, тогда вопрос, почему отрабатывает метод?

Потому что это undefined behavior — т.е. стандарт не определяет, как себя должна при это вести программа.

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

rumgot ★★★★★

(10.06.21 18:04:20 MSK)

  • Ссылка

Ответ на:

комментарий
от da17 10.06.21 18:03:02 MSK

Ответ на:

комментарий
от da17 10.06.21 18:03:02 MSK

Методы класса — это обычные функции, что ты хочешь очищать?

  • Ссылка

Ответ на:

комментарий
от da17 10.06.21 18:03:02 MSK

А почему тогда память где остается реализация методов класса не очищается?

Реализация методов класса — это код, который никогда не изменяется и никак не зависит от времени жизни объектов класса. Если ты имел в виду данные объекта, то зачем это они должны очищаться?

В твоём примере ты вообще не обращаешься к this, и состояние объекта ни на что не влияет. А мог бы даже успешно обратиться к полю, поскольку память где был расположен объект могла остаться доступной программе и даже не быть кем-то переписанной. В другом случае мог бы прочитать мусор, в третьем упасть, в четвёртом это даже не скомпилируется.

slovazap ★★★★★

(10.06.21 18:05:33 MSK)



Последнее исправление: slovazap 10.06.21 18:09:24 MSK
(всего

исправлений: 5)

  • Ссылка

Ответ на:

комментарий
от invy 10.06.21 18:17:39 MSK

Да.

★★★★★

Этот мир не спасти…

fsb4000 ★★★★★

(10.06.21 18:20:48 MSK)

  • Ссылка

Ответ на:

комментарий
от invy 10.06.21 18:17:39 MSK

Ничего непонятно, у меня деструктор вызывается сразу после возврата из ф-ии.

da17

(10.06.21 18:33:26 MSK)

  • Показать ответы
  • Ссылка

Ответ на:

комментарий
от da17 10.06.21 18:33:26 MSK

Ну и что? Ты хочешь разобраться в том, как именно реализовано неопределенное поведение в разных компиляторах? Может будем учиться писать правильно, а не писать неправильно и разбираться, что там будет происходить?

rumgot ★★★★★

(10.06.21 18:35:45 MSK)

  • Ссылка

Ответ на:

комментарий
от invy 10.06.21 18:17:39 MSK

Да.

Нет.

Const reference продлевает жизнь обьекта.

До возврата из функции return. Иначе, например, можно было бы совершенного легально передавать ссылки на локальные переменные.

anonymous

(10.06.21 18:45:07 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от da17 10.06.21 18:33:26 MSK

Ничего непонятно

Я понял, у тебя интернета нет, кроме лора. Ты не можешь перейти на годболт.

const ссылка на временный объект (комментарий)

https://gcc.godbolt.org/z/oTf5EY38r

<source>: In function 'const A& foo()':
<source>:9:16: warning: returning reference to temporary [-Wreturn-local-addr]
    9 |         return A();
      |                ^~~

/app/example.cpp:9:18: runtime error: reference binding to null pointer of type 'const struct A'
/app/example.cpp:13:14: runtime error: member call on null pointer of type 'const struct A'

fsb4000 ★★★★★

(10.06.21 18:51:20 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от anonymous 10.06.21 18:45:07 MSK

Ответ на:

комментарий
от invy 10.06.21 18:17:39 MSK

The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11), see reference initialization for details

Но если прочитать про этот reference initialization, то можно выяснить, что:

There are following exceptions to this lifetime rule:

  • a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such return statement always returns a dangling reference.
  • Показать ответ
  • Ссылка

Все будет работать, если заменишь на:

PRN

(10.06.21 19:52:46 MSK)

  • Ссылка

ну в общих чертах такой код лишен смысла — ибо к чему возвращать временный объект по ссылке, тем более по константной?

по поводу отработки метода — он принадлежит классу, а не объекту класса, — на него поинтер всегда существует, это просто участок кода, а не стейт объекта — в ассемблере это просто jmp обычно, ну или инлайн.

safocl ★★

(11.06.21 02:20:35 MSK)



Последнее исправление: safocl 11.06.21 02:22:52 MSK
(всего

исправлений: 1)

  • Ссылка

Ответ на:

комментарий
от fsb4000 10.06.21 18:51:20 MSK

Ну я плохо ассемблер современный понимаю и по-английски совсем не читаю, у нас в школе и инсте французский был. В общем так писать нельзя, хотя работает. Я понял. Зачем вопрос не стоит, вопрос стоит по-стандарту это ок или запретили уже.

da17

(11.06.21 17:55:03 MSK)



Последнее исправление: da17 11.06.21 17:57:27 MSK
(всего

исправлений: 1)

  • Ссылка

Ответ на:

комментарий
от invy 10.06.21 18:17:39 MSK

Как же продлевает, если у меня мусор в членах класса. Компилирую gcc с опцией -std=c++11 как-то так

da17

(11.06.21 17:58:44 MSK)

  • Показать ответ
  • Ссылка

На вопрос уже ответили, я не помешаю доп вопросом со «звездочкой»?
Что за хитропопую оптимизацию делает вектор #1?

#include <vector>
#include <cstdio>
#include <cstdlib>
using namespace std;


void* operator new(std::size_t sz)
{
    std::printf("global op new called, size = %zun", sz);
    if (sz == 0)
        ++sz;
    if (void *ptr = std::malloc(sz))
        return ptr;
    throw std::bad_alloc{};
}

int main()
{
	// #1
	//global op new called, size = 80
	vector<int> v;
	v.reserve(20);
	v = {3, 4};
	
	// #2
	//global op new called, size = 80
	//global op new called, size = 8
	vector<int> v2;
	v2.reserve(20);
	v2 = vector<int>{3, 4};

	return 0;
}

Почему не создаётся промежуточный вектор? Т.е. получается, что второй раз конструктор отрабатывает на уже инициализированном векторе.

anonymous

(11.06.21 18:42:11 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от anonymous 11.06.21 18:42:11 MSK

Метод boo отрабатывает, но вот объект разрушается раньше, тогда вопрос, почему отрабатывает метод?

Видите ли, если вы для class создадите объект с помощью malloc вместо new, то код работать будет, но частенько и crash будет в непонятно какие моменты …

anonymous

(11.06.21 19:02:42 MSK)

  • Ссылка

Ответ на:

комментарий
от fsb4000 11.06.21 18:51:40 MSK

Так constexpr вектора пока только лишь мелкомягкие запилили, это не мой случай и такое не работает — constexpr vector i{4}; Да не ясно почему в разных случаях (#1, #2) поведение разное.

anonymous

(11.06.21 19:07:55 MSK)

  • Показать ответы
  • Ссылка

Ответ на:

комментарий
от da17 11.06.21 17:58:44 MSK

Ну ты умкдрился выбрать вариант где не продлевает :)

invy ★★★★★

(11.06.21 22:05:08 MSK)

  • Ссылка

Ответ на:

комментарий
от anonymous 11.06.21 19:07:55 MSK

Почитай про move. У тебя случается аллокация как раз для временного вектора. Но когда ты его присваиваешь переменной, происходит тупо свап указателей. В итоге временный вектор становится пустым (точнее в нем буфер на 20 элементов от старого вектора, но size == 0, а через мгновение вызовется деструктор), а вектор в переменной использует его память.

Даже без move непонятно, зачем старому вектору делать аллокацию, если ты сделал reserve на 20 элементов, а присваиваешь вектор из 2 элементов. Даже если случится обычный copy (в C++ до 11 версии), переаллокация памяти не нужна.

Constexpr для этого совсем не нужен.

KivApple ★★★★★

(12.06.21 01:19:39 MSK)



Последнее исправление: KivApple 12.06.21 01:23:07 MSK
(всего

исправлений: 3)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от da17 10.06.21 18:03:02 MSK

потому что это с++, тут принято экономить на всяких ненужных действиях чтобы было быстрее.

anonymous

(12.06.21 01:49:22 MSK)

  • Ссылка

Ответ на:

комментарий
от KivApple 12.06.21 01:19:39 MSK

Прежде чем делать move, временный вектор нужно создать, а это аллокация как ни крути, которая отметится в операторе new (но нет). Единственное объяснение (которое мне на ум приходит) — вектор создается в компайлтайме (constexpr), но ГЦЦ со Шлангом этого пока не умеют. Также не ясно — почему в первом случае «constexpr», а во втором нет, я лишь записал там в explicit форме то же самое. К тому же всего одна аллокация и в таком случае:

	int a; 
	cin >> a;
	// #1
	//global op new called, size = 80
	vector<int> v;
	v.reserve(20);
	v = {a};

Никакой constexpr здесь быть не может.

anonymous

(12.06.21 03:24:16 MSK)

  • Показать ответы
  • Ссылка

Ответ на:

комментарий
от anonymous 11.06.21 19:07:55 MSK

Так constexpr вектора пока только лишь мелкомягкие запилили

При чём тут это?

Да не ясно почему в разных случаях (#1, #2) поведение разное.

Тебе ясно показали, почему. Просто ты довн.

anonymous

(12.06.21 05:02:33 MSK)

  • Ссылка

Ответ на:

комментарий
от anonymous 12.06.21 03:24:16 MSK

Никакой constexpr здесь быть не может.

constexpr int f(int i) { return i; }

#include <iostream>
using namespace std;

int main()
{
	int a;
	cin >> a;
	return f(a); // и здесь тоже не может?
}

anonymous

(12.06.21 05:10:56 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от anonymous 12.06.21 05:10:56 MSK

Ладно, похоже я не в том месте вопрос задал. Люди вообще не понимают темы компайл тайм строк и векторов, срабатывают лишь какие-то рефлексы на знакомые слова. Мне не вопросы надо задавать, а ликбез проводить.

anonymous

(12.06.21 06:27:03 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от anonymous 12.06.21 06:27:03 MSK

Ладно, похоже я не в том месте вопрос задал.

Ты на https://cppinsights.io/ перешёл или нет?

Что за люди то тут такие, не могут по одной ссылке перейти. Что оп, что ты.

Вот как твой main выглядит в cppinsight.

int main()
{
  vector<int> v = std::vector<int, std::allocator<int> >();
  v.reserve(20);
  v.operator=(std::initializer_list<int>{3, 4});
  vector<int> v2 = std::vector<int, std::allocator<int> >();
  v2.reserve(20);
  v2.operator=(std::vector<int, std::allocator<int> >{std::initializer_list<int>{3, 4}, std::allocator<int>()});
  return 0;
}

Теперь то понятно, почему std::vector во втором случае создаётся, а в первом нет?

И в моём первом ответе нужно было смотреть не на constexpr(он есть у обоих перегрузок оператора =), а на тип который он принимает.

std::initializer_list<T> ilist

и

fsb4000 ★★★★★

(12.06.21 06:34:55 MSK)



Последнее исправление: fsb4000 12.06.21 06:44:42 MSK
(всего

исправлений: 1)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от fsb4000 12.06.21 06:34:55 MSK

Теперь то понятно, почему std::vector во втором случае создаётся, а в первом нет?

Нет, не понятно. Запись вида

v.operator=(std::initializer_list<int>{3, 4});

«трансформируется» точно в такую же:

v2.operator=(std::vector<int, std::allocator<int> >{std::initializer_list<int>{3, 4}, std::allocator<int>()});

Вызов конвертирующего конструктора с последующем перемещенимем. Первая форма — сокращение от второй, т.к. позволено из отсутствия explicit у конструктора. Единственный фокус провернуть такую — штуку — создать вектор в компайл тайме, а потом лишь скопировать (пройдёт мимо моего оператора new, хотя пример с cin не подтверждает это). Короче не знаю что за фокусы такие происходят.

anonymous

(12.06.21 07:00:58 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от anonymous 12.06.21 07:00:58 MSK

Ответ на:

комментарий
от fsb4000 12.06.21 07:06:20 MSK

Понял, не заметил (честно говоря и не ожидал, что у вектора есть такая сигнатура:

vector& operator=( std::initializer_list<T> ilist );

Какое-то туннельное мышление, инициализер лист плотно ассоциировался с конструктором, никак не с оператором =. Хотя логично проверить было сразу, но шоры …

Спасибо.

anonymous

(12.06.21 07:23:02 MSK)

  • Показать ответ
  • Ссылка

Ответ на:

комментарий
от anonymous 12.06.21 07:23:02 MSK

Ответ на:

комментарий
от fsb4000 10.06.21 17:41:23 MSK

const ссылка на временный объект

Жуть!

anonymous

(12.06.21 08:59:06 MSK)

  • Ссылка

Ответ на:

комментарий
от anonymous 12.06.21 03:24:16 MSK

Я про второй случай. Тут у тебя вполне есть две аллокации.

//global op new called, size = 80
	//global op new called, size = 8

В первом случае вектор вообще не создаётся. У тебя там std::initializer_list, а он создаётся статически. Constexpr для этого не нужен.

KivApple ★★★★★

(12.06.21 12:17:43 MSK)



Последнее исправление: KivApple 12.06.21 12:18:03 MSK
(всего

исправлений: 1)

  • Ссылка

Ответ на:

комментарий
от aldaril_kote 10.06.21 19:20:14 MSK

The lifetime of a temporary object may be extended by binding to a const lvalue reference or to an rvalue reference (since C++11), see reference initialization for details

Но если прочитать про этот reference initialization, то можно выяснить, что:

There are following exceptions to this lifetime rule:

  • a temporary bound to a return value of a function in a return statement is not extended: it is destroyed immediately at the end of the return expression. Such return statement always returns a dangling reference.

А если почитать стандарт, то можно выяснить, что функции не возвращают ссылок. Ни dangling, ни не dangling.

anonymous

(12.06.21 17:38:14 MSK)

  • Ссылка

Ответ на:

комментарий
от x86- 11.06.21 03:55:24 MSK

Нихрена нельзя. Надо понимать одно: в c++ на уровне языка нет никакой мув-семантики. Она реализуется библиотечно, функциями. Для компилятора это просто особый тип ссылки, type&&, к которому автоматически преобразовывается выражение не являющееся именем переменной, и которая может быть отправлена в функцию принимающую type&&. Все кажущиеся логичными предположения о том как должен работать мув являются неверными. Все разговоры о том, что рвалуе ссылка чего-то там продлевает — неверны. Точнее они верны но только в узком смысле, продление жизни происходит только в достаточной степени чтоб хватило на передачу, на выход из функции это не распространяется. Более того, результат вызова функции, возвращающей объект по значению как раз является временным неименованным объектом и потому автоматически кастится к рвалуе ссылке, так что возврат рвалуе значения в принципе не имеет смысла, за исключением передачи назад рвалуе ссылки полученной на входе. Таким образом если тебе хочется создать объект в функции и передать его наружу мувом — просто возвращай по значению.

khrundel ★★★★

(13.06.21 10:42:08 MSK)

  • Ссылка

27 сентября 2021 г.

Настигла меня карма, на собеседовании дали этот вопрос, а все что я мог сказать, только то, что «так нельзя делать». Не разобрался до конца, потом еще начали const убирать добавлять и т.д. прося объяснить, что будет.

da17

(27.09.21 10:57:21 MSK)

  • Ссылка

Цитата:

Ну так это лишнее — создавать второй объект, когда нужен один. Как записать, чтобы только вызывался конструктор?

В записи

Код:

res = Foo( x, f(x) );

необходимо четко разделять создание нового объекта и присваивание(копирование) объекту res только что созданного объекта. Боюсь, что мы углубимся в дебри, но опять подчеркну, уже в этой теме, возможность перегрузки оператора присваивания Foo& operator=(const Foo&)

. Поэтому компилятор должен сначала создать новый объект, а потом выполнить Foo.operator=

, это бывает принципиально, поэтому в общем случае компилятор не может сразу так вот создавать на месте res новую инстанцию объекта Foo.

Цитата:

Ну так это лишнее — создавать второй объект, когда нужен один.

Согласен с Вами.

Цитата:

Как записать, чтобы только вызывался конструктор? Почему запись

Код:

res.Foo(x,f(x));

ошибочна?

Запись ошибочна потому, что в Си++ запрещено вызывать конструктор класса (также как и деструктор) напрямую. Они вызываются либо автоматически, как в Вашем случае приведения типов, либо с помощью операторов new/delete

при динамическом создании объектов.

Проблема Вашего класса, насколько я понял, в отсутствии, кроме конструктора, функции преобразования значений x, f(x) в объект класса Foo. Если Вы не имеете доступа к реализации класса Foo, то Вам ничего не остается, как создавать объект каждый раз заново.

От операции присваивания(копирования) объекта Foo избавиться можно, если воспользоваться методом, предложенным maxal

‘ом, т.е. вместо присваивания объекта Foo другому объекту Foo можно присваивать адрес созданного объекта Foo указателю, это не ресурсоемкая операция. Однако, не рекомендую. Криво ужасно получается, деструктор где-то вызывать и т.п. Может быть, Вам стоит перекомпановать Вашу функцию, чтобы в ней уже производились какие-то полезные действия с вновьсозданным объектом, и в ней же он потом и удалялся?

Вопрос в другом — а настолько ли это принципиально? Память тут роли не играет (если у Вас конечно одна инстанция объекта не занимает половину доступной памяти :D ), т.к. память, отводимая под новый объект, сразу же после операции присваивания освобождается. Скорость — вот что надо оценить. Если у Вас подобные действия происходят в цикле, объект сложный, то есть смысл думать, как бы пооптимизировать. А если операция присваивания в среднем занимает, скажем, 0,1% от всего времени работы программы, то и думать над ней не нужно, пока Вы не оптимизируете более ресурсоемкие участки кода.

AdlerSam


  • #1

Hi,

I wonder why the following two lines produce a warning:

class X {};
const X &f() {return X();}

$ g++ -c ref.cpp
ref.cpp: In function ‘const X& f()’:
ref.cpp:2: warning: returning reference to temporary

As far as I understand, a const reference _extends_ the lifetime of a
temporary until the very last reference instance that refers to the
temporary goes out of scope. Thus, where is the problem that justyfies
the warning?

Advertisements

domachine


  • #2

Hi,

I wonder why the following two lines produce a warning:

class X {};
const X &f() {return X();}

$ g++ -c ref.cpp
ref.cpp: In function ‘const X& f()’:
ref.cpp:2: warning: returning reference to temporary

As far as I understand, a const reference _extends_ the lifetime of a
temporary until the very last reference instance that refers to the
temporary goes out of scope. Thus, where is the problem that justyfies
the warning?

Who told you, the thing with the lifetime? I think you misunderstood
something. The const does to a reference, is making it constant.
Comparable to constant pointers.

class Test
{
public:
void non_const_method()
{
// Do something like writing to a member-variable
// …
}

void const_method() const
{
// Do something constant like reading a variable
// …
}
}

int main()
{
const Test& ref = Test();

ref.non_const_method(); // This doesn’t work
ref.const_method(); // Yes this does
}

This is the only thing the const keyword does. We don’t have a garbage
collector in C++.

Best regards Dominik

domachine


  • #3

Who told you, the thing with the lifetime? I think you misunderstood
something. The const does to a reference, is making it constant.
Comparable to constant pointers.

class Test
{
public:
    void non_const_method()
    {
        // Do something like writing to a member-variable
        // …
    }

    void const_method() const
    {
        // Do something constant like reading a variable
        // …
    }

}

int main()
{
    const Test& ref = Test();

    ref.non_const_method();  // This doesn’t work
    ref.const_method();  // Yes this does

}

This is the only thing the const keyword does. We don’t have a garbage
collector in C++.

Best regards Dominik

Sorry there’s a mistake in my main.

It should be:

int main()
{
Test test;
const Test& ref = test;

ref.non_const_method(); // This doesn’t work
ref.const_method(); // Yes this does

}

Paul Brettschneider


  • #4

AdlerSam said:

Hi,

I wonder why the following two lines produce a warning:

class X {};
const X &f() {return X();}

$ g++ -c ref.cpp
ref.cpp: In function ‘const X& f()’:
ref.cpp:2: warning: returning reference to temporary

As far as I understand, a const reference _extends_ the lifetime of a
temporary until the very last reference instance that refers to the
temporary goes out of scope. Thus, where is the problem that justyfies
the warning?

This assumption is — of course — nonsense. If you want to manage lifetime of
objects, you usually use smart pointers or containers.

Hope that helps.

AdlerSam


SG


  • #6

Hm — Then where do I have mistaken Herb Sutters GotW #88:?

GotW #88: A Candidate For the “Most Important const”

To quote the important part:

This is just a simplification of the C++ rules. It only applies to
cases like

string source();

void test() {
string const& x = source();
// x still refers to a valid string object
cout << x << endl;
}

However, returning references to function-local objects is never ok,
NEVER.

Cheers!
SG

Advertisements

Fred Zwarts


  • #7

AdlerSam said:

Hi,

I wonder why the following two lines produce a warning:

class X {};
const X &f() {return X();}

$ g++ -c ref.cpp
ref.cpp: In function ‘const X& f()’:
ref.cpp:2: warning: returning reference to temporary

As far as I understand, a const reference _extends_ the lifetime of a
temporary until the very last reference instance that refers to the
temporary goes out of scope. Thus, where is the problem that justyfies
the warning?

Where does the reference (the return value in this case) go out of scope?

AdlerSam


  • #8

Ok, got it, thanks for your help. I just got confused by the quoted
article, making me think that references may behave «smarter» than
plain pointers in that they may prevent the destruction of local
variables until they themselves go out of scope.

Bart van Ingen Schenau


  • #9

AdlerSam said:

Hm — Then where do I have mistaken Herb Sutters GotW #88:?

http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-
const/

To quote the important part:

expression in which it appears. However, C++
reference to const on the stack lengthens the lifetime of
avoids what would otherwise be a common dangling
by f() lives until the closing curly brace. (Note this
references that are members of objects.)

You go wrong when you assume this lifetime extension is transitive.
The lifetime of a temporary is indeed extended, but only to the lifetime of
the *initial* reference it is bound to.
If you use that reference to initialise a second reference, then the
lifetime of the temporary is not further extended.

In your initial example, the temporary is bound to the reference being
returned, so the lifetime of the temporary is extended to the lifetime of
the return value.
As this creates a great risk of getting a dangling reference (for example,
when you have the code «const X& x = f();»), most compilers will warn you
when you try to return a reference to something that won’t live long enough
to be useful in the caller.

Bart v Ingen Schenau

AdlerSam


  • #10

You go wrong when you assume this lifetime extension is transitive.

Thanks particularly for this one sentence: It just explains everything
and puts my «gut feeling» on when references extend the lifetime of a
temporary and when it doesn’t back on solid grounds!

Advertisements

James Kanze


  • #11

That would require full garbage collection, and then some, in
order to implement.

Hm — Then where do I have mistaken Herb Sutters GotW #88:?

To quote the important part:

You seem to misunderstand two important points:

— first, the lifetime of the temporary is extended only to the
end of the lifetime of the reference the temporary
initializes, not to any other references, and

— second, return involves a copy, so the reference being
returned is not the reference the temporary initialized.

Herb has simply used a common, but misleading formulation of the
rule, which is better stated as «initializing a reference with
a temporary extends the lifetime of the temporary to that of the
reference». When returning a reference, you (formally, at
least) initialize a local temporary reference with the return
expression, then return a copy of that reference, with the local
temporary reference going out of scope (and thus triggering the
destruction of the temporary used to initialize it).

Advertisements

Понравилась статья? Поделить с друзьями:
  • Error return statement with a value in function returning void fpermissive
  • Error retrieving settings from server valorant
  • Error retrieving network interface
  • Error retrieving information from server df dferh 01 что делать
  • Error retrieving database metadata