Skip to content

Latest commit

 

History

History
165 lines (128 loc) · 8.58 KB

for_loop.md

File metadata and controls

165 lines (128 loc) · 8.58 KB

Синтаксический сахар с ложкой дегтя: range-based for

Как мы уже выяснили ранее, константные 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++, пока его не починят

С++23

Продление времени жизни объекта в заголовке range-based-for было наконец-то исправлено. И теперь получить висячую ссылку стало тяжелее. Но теперь стало проще получить другую проблему

Полезные ссылки

  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