Files
ubbook/lifetime/for_loop.md
T
2022-03-24 14:03:41 +03:00

161 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Синтаксический сахар с ложкой дегтя: range-based for
Как мы уже выяснили ранее, константные lvalue (да и rvalue тоже) ссылки доставляют много радости в C++ благодаря правило продления жизни для временных объектов.
Правило хитрое и состоит не только в том, что `const&&` или `&&` продляют жизнь временному объекту (но только первая такая ссылка). На самом деле правило такое:
все временные объекты живут до окончания выполнения всего включающего их выражения (statement) — грубо говоря, до ближайшей точки с запятой(`;`).
ИЛИ же до окончания области видимости первой попавшейся на пути у этого временного объекта `const&` или `&&` ссылки, если область видимости ссылки больше, чем время жизни этого самого временного объекта.
То есть:
```C++
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` становится висячей. Занавес.
```
Вооружившись полученным пониманием, рассмотрим другой пример и перестанем опять все понимать:
```C++
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";
}
}
```
Все [работает](https://godbolt.org/z/r1zbzK), как и ожидается
Повысим инкапсуляцию, проведем минимальный рефакторинг — сделаем `vertexes` приватным полем с read-only доступом:
```C++
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";
}
}
```
И все [сломалось](https://godbolt.org/z/Ejq745). В коде неопределенное поведение.
Как же так? Разгадка в том, что, несмотря на то, что заголовок range-based for выглядит как единое выражение, пишется и воспринимается как единое выражение, единым выражением он не является.
С 17 стандарта и дальше конструкция
```C++
for (T v : my_cont) {
...
}
```
Рассахаривается в примерно следующее:
```C++
auto&& container_ = my_cont; // sic!
auto&& begin_ = std::begin(container_);
auto&& end_ = std::end(container_);
for (; begin_ != end_; ++begin_) {
T v = *begin_;
}
```
В первом случае
```C++
auto&& container_ = MakeShape().vertexes;
// временный объект Shape живет до ;. Он не встретил еще ни одной const& или &&
// ссылки
// Подобъект vertexes — считается таким же временным.
// Его время жизни закончится на ;
// Но он встречает && ссылку, область видимости которой простирается ниже
// и продлевает ему жизнь. Причем продлевается жизнь не только лишь подобъекту
// vertexes, а целиком временному объекту Shape, его содержащему
```
Во втором случае:
```C++
auto&& container_ = MakeShape().Vertexes();
// временный объект Shape живет до ;. Но он встречает неявную const&
// ссылку в методе Vertexes(). Ее область видимости ограничена телом метода.
// Продления жизни не происходит. Возвращается ссылка на часть временного объекта
// и присваивается ссылке `container_`.
// Дело доходит до ;. Временный Shape умирает.
// `container_` становится висячей ссылкой. Занавес.
```
Вот так все просто и сломано.
Кстати говоря: механизм продления жизни объекту с помощью ссылки на его подобъект — очень неочевидная штука. И, если, например, ваш код полагается на какие-то эффекты в деструкторах, можно получить не совсем то, [чего хотите](https://godbolt.org/z/9M946o).
---
Как избежать проблемы с range-based for?
- Никогда не забывать делать rvalue [перегрузку](https://godbolt.org/z/TPYzEj) для любых const-методов
- Никогда не использовать никакие выражения после `:` в заголовке цикла. Только переменные или их поля.
- В C++20 использовать синтаксис range-based-for с инициализатором
```C++
for (auto cont = expr; auto x : cont)
```
- При использовании синтаксиса с инициализатором думать, прежде чем использовать
`auto&&` или `const auto&` для инициализатора. Впрочем, это не только про for...
- Использовать `std::ranges::for_each`
- Не использовать range-based for в C++, пока его не починят
## Полезные ссылки
1. https://en.cppreference.com/w/cpp/algorithm/ranges/for_each
2. https://en.cppreference.com/w/cpp/language/range-for
3. https://en.cppreference.com/w/cpp/language/reference_initialization#Lifetime_of_a_temporary
4. http://josuttis.com/download/std/D2012R0_fix_rangebasedfor_201029.pdf