Как избежать неопределенного поведения: временные объекты - PullRequest
1 голос
/ 29 мая 2020

Я написал класс для использования его в качестве удобного представления, например, в for s на основе диапазона. В целом, это всего лишь пара итераторов с проверкой привязки:

template<typename I> class Range {
private:
  I begin;
  I end;

public:
  Range(I const begin, I const end)
    : begin(begin), end(end)
  {}

  Range<I>& skip(int const amount) {
    begin = std::min(begin + amount, end);
    return *this;
  }
};

template<typename C> auto whole(C const& container) {
  using Iterator = decltype(std::begin(container));
  return Range<Iterator>(std::begin(container), std::end(container));
}

Вот его предполагаемое использование (которое привело к UB):

std::vector<int> const vector{1, 2, 3};
for (int const value : whole(vector).skip(1)) {
  std::cout << value << ' ';
}

Удаление части skip(1) помогает, то же самое касается следующего рефакторинга Range::skip:

Range<I> skip(int const amount) const {
  I const new_begin = std::min(begin + amount, end);
  return Range<I>(new_begin, end);
}

Похоже, временное не должно возвращать ссылку на себя. Вот что говорит cppreference:

Все временные объекты уничтожаются на последнем этапе оценки полного выражения, которое (лексически) содержит точку, в которой они были созданы, и если было создано несколько временных объектов, они уничтожаются в порядке, противоположном порядку создания.

Хотя я не уверен, что это так, и я не знаю, как интерпретировать это практически. В чем реальная проблема и как я могу надежно избежать такого UB? Подобные выражения, например auto string = std::string("abc").append("def"), тоже небезопасны?

Ответы [ 3 ]

1 голос
/ 29 мая 2020

В на основе диапазона для l oop выражение диапазон привязано к ссылке как (код прототипа)

auto && __range = whole(vector).skip(1) ;

Проблема в том, что временное, созданное whole(vector), уничтожается сразу после полного выражения, ссылка __range (которая привязывается к возвращенной ссылке из skip, т.е. временная) становится висячей; после этого любое разыменование на нем приводит к UB.

auto string = std::string("abc").append("def") нормально, string копируется, это независимый объект от временного std::string.

Начиная с C ++ 20 вы можете добавить init-statement :

Если range_expression возвращает временное значение, его время жизни продлевается до конца l oop, на что указывает привязка к ссылке пересылки __ range , но помните, что время жизни любого временного выражения в пределах range_expression не расширено.

Например

std::vector<int> const vector{1, 2, 3};
for (auto thing = whole(vector); int const value : thing.skip(1)) {
  std::cout << value << ' ';
}
1 голос
/ 29 мая 2020

Диапазон-for содержит ссылку на диапазон. Ваш пример

for (int const value : whole(vector).skip(1)) {
  std::cout << value << ' ';
}

определяется как эквивалент

{
    auto && __range = whole(vector).skip(1);
    auto __begin = __range.begin();
    auto __end = __range.end();
    for ( ; __begin != __end; ++__begin) {
        int const value = *__begin;
        std::cout << value << ' ';
    }
}

Ссылка, возвращаемая skip, становится недействительной при первом ;.

Где вы инициализируют объект, например

std::string s = std::string("abc").append("def");

, это безопасно, потому что временному нужно только пережить конструктор.

В стороне, я предпочитаю Range<I> skip(int) const изменяющемуся.

0 голосов
/ 29 мая 2020

Что касается std::string и функции append , функция append возвращает ссылку в строку, к которой она присоединяется. Сохранение этой ссылки после уничтожения строкового объекта приведет к неопределенному поведению, если вы его используете.

Однако , если вы скопируете строковый объект, вы в безопасности, потому что у вас будет копия строки, а не ссылка на несуществующий объект:

std::string s = std::string("abc").append("def");

Здесь s будет копией , инициализированной посредством инициализации копирования (он передает std::string("abc").append("def") конструктору копирования для s, и временный объект будет существовать на всем протяжении этого конструктора).


Как для

for (int const value : whole(vector).skip(1)) { ... }

Если Range<T> класс был изменен для итерации (вам нужны begin и end функции для возврата итераторов), тогда это все равно не будет UB.

Это потому, что такие диапазон-для l oop соответствует al oop как

for (auto iter = whole(vector).skip(1).begin();
     iter != whole(vector).skip(1).end();
     ++iter)
{
    ...
}

Класс Range<T> не содержит копию вектора, он содержит копии итератора вектора (для показанного вами примера). Эти итераторы будут скопированы или использованы до временный объект Range<T> будет уничтожен.

...