mirror of
https://github.com/Nekrolm/ubbook.git
synced 2026-06-23 13:26:42 +03:00
161 lines
8.2 KiB
Markdown
161 lines
8.2 KiB
Markdown
# Синтаксический сахар с ложкой дегтя: 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
|
||
|