В то время пока выходят статьи о сущности и подводных камнях 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)
разрушатся- создастся
o5
- деструкторы будут вызваны в обратном порядке:
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()
Спасибо за внимание, удачного дня.
Автор: abby