Как мы уже выяснили ранее, константные lvalue (да и rvalue тоже) ссылки доставляют много радости в C++ благодаря правило продления жизни для временных объектов.
Правило хитрое и состоит не только в том, что const&&
или &&
продляют жизнь временному объекту (но только первая такая ссылка). На самом деле правило такое:
все временные объекты живут до окончания выполнения всего включающего их выражения (statement) — грубо говоря, до ближайшей точки с запятой(;
).
ИЛИ же до окончания области видимости первой попавшейся на пути у этого временного объекта const&
или &&
ссылки, если область видимости ссылки больше, чем время жизни этого самого временного объекта.
То есть:
const int& x = 1 + 2; // временные объекты 1, 2,
// порождают временный объект 3 (сумма).
// Их время жизни закончится на ;
// Но мы присваиваем 3 константной ссылке,
// Ее область видимости простирается ниже, дальше ;
// Так что время жизни продлевается.
// Таким образом: 1, 2 — умирают. 3 — продолжает жить
const int& y = std::max([](const int& a, const int& b) -> const int& {
return a > b ? a : b;
}(1 + 2, 4), 5); // временные объекты 1,2, 3(сумма), 4, 5 живут до ЭТОЙ ;
// 3, 4 присваиваются константным ссылкам в аргументах лямбда-функии.
// область видимости этих ссылок заканчивается после return
// — она МЕНЬШЕ времени жизни временного объекта.
// ссылки ничего не продлили, но лишили временных объект будущего.
// 5 прибивается к константной ссылке в аргументе std::max
// Со ссылками на 4, 5 успешно отрабатывает std::max —
// их время жизни еще не закончилось. Ссылки валидны.
// Ссылка-результат присваивается `y`. Продлений жизни не происходит —
// все временные объекты уже безуспешно попытали счастья на аргументах функций.
// Дело доходит до ; Время жизни всех объектов 1,2,3,4,5 заканчивается.
// `y` становится висячей. Занавес.
Вооружившись полученным пониманием, рассмотрим другой пример и перестанем опять все понимать:
struct Point {
int x;
int y;
};
struct Shape {
public:
using VertexList = std::vector<Point>;
VertexList vertexes;
};
Shape MakeShape() {
return { Shape::VertexList{ {1,0}, {0,1}, {0,0}, {1,1} } };
}
int main() {
for (auto v : MakeShape().vertexes) {
std::cout << v.x << " " << v.y << "\n";
}
}
Все работает, как и ожидается
Повысим инкапсуляцию, проведем минимальный рефакторинг — сделаем vertexes
приватным полем с read-only доступом:
struct Shape {
public:
using VertexList = std::vector<Point>;
explicit Shape(VertexList v) : vertexes(std::move(v)) {}
const VertexList& Vertexes() const {
return vertexes;
}
private:
VertexList vertexes;
};
...
int main() {
for (auto v : MakeShape().Vertexes()) {
std::cout << v.x << " " << v.y << "\n";
}
}
И все сломалось. В коде неопределенное поведение.
Как же так? Разгадка в том, что, несмотря на то, что заголовок range-based for выглядит как единое выражение, пишется и воспринимается как единое выражение, единым выражением он не является.
С 17 стандарта и дальше конструкция
for (T v : my_cont) {
...
}
Рассахаривается в примерно следующее:
auto&& container_ = my_cont; // sic!
auto&& begin_ = std::begin(container_);
auto&& end_ = std::end(container_);
for (; begin_ != end_; ++begin_) {
T v = *begin_;
}
В первом случае
auto&& container_ = MakeShape().vertexes;
// временный объект Shape живет до ;. Он не встретил еще ни одной const& или &&
// ссылки
// Подобъект vertexes — считается таким же временным.
// Его время жизни закончится на ;
// Но он встречает && ссылку, область видимости которой простирается ниже
// и продлевает ему жизнь. Причем продлевается жизнь не только лишь подобъекту
// vertexes, а целиком временному объекту Shape, его содержащему
Во втором случае:
auto&& container_ = MakeShape().Vertexes();
// временный объект Shape живет до ;. Но он встречает неявную const&
// ссылку в методе Vertexes(). Ее область видимости ограничена телом метода.
// Продления жизни не происходит. Возвращается ссылка на часть временного объекта
// и присваивается ссылке `container_`.
// Дело доходит до ;. Временный Shape умирает.
// `container_` становится висячей ссылкой. Занавес.
Вот так все просто и сломано.
Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект — очень неочевидная штука. И, если, например, ваш код полагается на какие-то эффекты в деструкторах, можно получить не совсем то, чего хотите.
Как избежать проблемы с range-based for?
- Никогда не забывать делать rvalue перегрузку для любых const-методов
- Никогда не использовать никакие выражения после
:
в заголовке цикла. Только переменные или их поля. - В C++20 использовать синтаксис range-based-for с инициализатором
for (auto cont = expr; auto x : cont)
- При использовании синтаксиса с инициализатором думать, прежде чем использовать
auto&&
илиconst auto&
для инициализатора. Впрочем, это не только про for... - Использовать
std::ranges::for_each
- Не использовать range-based for в C++, пока его не починят
Продление времени жизни объекта в заголовке range-based-for
было наконец-то исправлено. И теперь получить висячую ссылку стало тяжелее. Но теперь стало проще получить другую проблему