Pull to refresh

C++: Когда время жизни объекта определяется временем жизни ссылки на него

Reading time 3 min
Views 13K
В то время пока выходят статьи о сущности и подводных камнях r-value ссылок (пример со ссылками на полезные источники habrahabr.ru/post/157961) подозреваю, что довольно многие не знают особенности обычных l-value ссылок. Суть этой статьи показать пример, когда время жизни объекта определяется временем жизни l-value ссылки на него, и как это можно использовать. Если заинтересовало, то добро пожаловать. Кстати, зная как можно больше особенностей про l-value ссылки, будет проще понять r-value.

Можно считать, что все передают объекты по константной ссылке, когда это необходимо и довольно точно знают время жизни объекта.
Например:
struct S{};
void f(const S& value){} 
f(S());

В этом случае можно считать, что объект S() начнет разрушаться после вызова функции f(). Почему довольно точно? — Потому что, в случае q(A(), B()); нe определён порядок создания и, соответственно, разрушения объектов A и B. Так же все знают, что нельзя писать
int& r = 1; // не компилируется

А теперь самое интересное.
Но можно делать так:
const int& r = 1;
В этом случае согласно стандарту и Страуструпу (7.7.1)
  • сначала применяется неявная конвертация к типу int
  • затем значение складывается во временный объект типа int
  • а теперь этот временный объект используется для инициализации нашей ссылки
Т.е. в следующем примере
struct Obj
{
  Obj(int i) : m_i(i) {
    cout << "ctr: " << m_i << endl;
  }
  ~Obj() {
    cout << "dtr: " << m_i << endl;
  }
  Obj operator+(const Obj& value) {
    return Obj(m_i + value.m_i);
  }
  int m_i;
};

...

  Obj o1(1);
  const Obj& ro2 = Obj(2) + Obj(3);
  Obj o6(6);
  • создастся объект o1
  • создадутся Obj(2) и Obj(3) (последовательность стандартом не определяется)
  • создастся временный объект, которым проинициализируется ro2
  • Obj(2) и Obj(3) разрушатся
  • создастся o6
  • деструкторы будут вызваны в обратном порядке: o5, временный объект и o1

Вывод (msvs 2012):
ctr: 1
ctr: 3
ctr: 2
ctr: 5
dtr: 2
dtr: 3
ctr: 6
dtr: 6
dtr: 5
dtr: 1

Но и это еще не все. Так же все знают, зачем нужен виртуальный деструктор, но давайте рассмотрим следующий пример, когда у базового класса деструктор не виртуальный. Продолжим использовать наш Obj и добавим
struct D : Obj
{
  D(int i) : Obj(i) {
    cout << "D::ctr: " << m_i << endl;
  }
  ~D() {
    cout << "D::dtr: " << m_i << endl;
  }
};

  Obj o1(1);
  const Obj& ro2 = D(5);
  Obj o6(6);
Вывод:
ctr: 1
ctr: 5
D::ctr: 5
ctr: 6
dtr: 6
D::dtr: 5
dtr: 5
dtr: 1
Т.е. в этом случае, несмотря на то, что тип константной ссылки const Obj&, тем не менее, наш объект D “живет” пока “живет” ссылка на него.

Тут возникает вопрос: “А какая практическая польза?”. Один из ответов — это уже применяется в подходе ScopeGuard (http://www.drdobbs.com/cpp/generic-change-the-way-you-write-excepti/184403758?pgno=2). Я лично не стал бы использовать такой подход и обернул бы нужный “хендл” ресурса в класс с соответствующим деструктором и конструктором.

Ну как впечатления, а теперь вспомните про неявную конвертацию типов в случае, если конструктор не объявлен как explicit, выведение типов в шаблонных функциях и статью в начале поста.

Надеюсь, что кому-то эта статья открыла еще одну особенность С++.

Приложение и замечания.

Чтобы обезопасить тех, кто не полезет в стандарт и только начинающих С++ программистов, напоминаю, что в случае
const Obj& f() {return Obj();}
временный объект разрушится перед выходом из функции, и вернувшаяся ссылка будет битой. Время жизни объекта определяется только локальной ссылкой. Лаконичнее и нагляднее стандарта сказать будет труднее, если интересно, то начните с параграфа 12.2. Вот цитата из стандарта (которая довольно часто встречает во всяких багзиллах и форумах):
The second context is when a reference is bound to a temporary. The temporary to which the reference is bound or the temporary that is the complete object to a subobject of which the temporary is bound persists for the lifetime of the reference except as specified below. A temporary bound to a reference member in a constructor’s ctor-initializer (12.6.2) persists until the constructor exits. A temporary bound to a reference parameter in a function call (5.2.2) persists until the completion of the full expression containing the call.


В ходе написания статьи наткнулся на статью Х.Саттера http://herbsutter.com/2008/01/01/gotw-88-a-candidate-for-the-most-important-const/
Плюс интересный пример http://www.rsdn.ru/forum/cpp/4257549.flat
#include <iostream>
struct foo {
    ~foo() {
        std::cout << "~foo()\n";
    }
};

struct foo_holder {
    const foo &f;
};

int main() {
    foo_holder holder = { foo() };
    std::cout << "done!\n";
    return 0;
}
Я бы предположил, что вывод должен быть
~foo()
done!

Потому что в этом случае временный объект используется в выражении, которое является инициализатором, а тогда, как в случае с обычными функциями, время жизни временного объекта не распространяется дольше выражения.
Но на практике результат немного другой.
Вывод (msvs 2012):
~foo()
done!
~foo()
И (g++ (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3):
done!
~foo()


Спасибо за внимание, удачного дня.
Tags:
Hubs:
+24
Comments 12
Comments Comments 12

Articles