C ++ 11 итератор и объем возвращаемого std :: unique_ptr

проблема

Как я понимаю, когда std::unique_ptr возвращается из функции в rvalue, его время жизни должно включать в себя оператор, который использует это значение. Но при компиляции с gcc 6.4.1 возвращается значение из Foo::iterator() выходит за рамки, прежде чем Начните оператора foreach в C ++ 11 в функции crashing_version(), Как показано в выходных данных ниже, деструктор вызывается сразу после вычисления содержащего выражения. Это ошибка в gcc или плохая практика программирования?

Случай использования

Цель этого шаблона — сделать итерацию доступной без раскрытия частных векторов. Это, кажется, требует некоторого объекта, такого как Foo::Iterator потому что есть два отдельных списка для повторения.

#include <iostream>
#include <memory>
#include <vector>

class Foo {
/* Goal: allow iteration without exposing the vector objects. */
std::vector<int> _list0;
std::vector<int> _list1;

public:
class Iterator {
int _list_id;
Foo& _foo;

public:
Iterator(int list_id, Foo& foo) : _list_id(list_id), _foo(foo) {}
~Iterator() {
std::cout << "~Iterator(): Destroying iterator of the "<< (_list_id == 0 ? "even" : "odd") << " list\n";
}

std::vector<int>::iterator begin() {
if (_list_id == 0)
return _foo._list0.begin();
else
return _foo._list1.begin();
}

std::vector<int>::iterator end() {
if (_list_id == 0)
return _foo._list0.end();
else
return _foo._list1.end();
}
};

void add(int i) {
if ((i % 2) == 0)
_list0.push_back(i);
else
_list1.push_back(i);
}

std::unique_ptr<Iterator> iterator(int list_id) {
return std::make_unique<Iterator>(list_id, *this);
}
};

void working_version() {
Foo foo;

for (int i = 0; i < 10; i++)
foo.add(i);

/* This works because the unique_ptr stays in scope through the loop. */
std::cout << "Valid iterator usage: \n";
std::unique_ptr<Foo::Iterator> evens = foo.iterator(0);
for (int i : *evens)
std::cout << i << "\n";
}

void crashing_version() {
Foo foo;

for (int i = 0; i < 10; i++)
foo.add(i);

/* Crash! The unique_ptr goes out of scope before the loop starts. */
std::cout << "Corrupt iterator usage: \n";
for (int i : *foo.iterator(1))
std::cout << i << "\n";
}

int main() {
working_version();
crashing_version();

return 0;
}

Выход программы:

Valid iterator usage:
0
2
4
6
8
~Iterator(): Destroying iterator of the even list

Corrupt iterator usage:
~Iterator(): Destroying iterator of the odd list
1
3
5
7
9

2

Решение

for(range_declaration:range_expression) выражение эквивалентно (в а также ) к:

{
auto && __range = range_expression ;
for (
auto __begin = begin_expr, __end = end_expr;
__begin != __end;
++__begin)
{
range_declaration = *__begin;
loop_statement
}
}

источник, с переменными, начинающимися с __ существующий только в качестве экспозиции

Мы заменяем:

for (int i : *evens)
std::cout << i << "\n";

и мы получаем:

{
auto && __range = *evens;
for (
auto __begin = begin_expr, __end = end_expr;
__begin != __end;
++__begin)
{
int i = *__begin;
std::cout << i << "\n";
}
}

Теперь мы можем ясно увидеть вашу ошибку. Ваш уникальный Ptr длится до тех пор, пока __range линии, но после разыменования уникальный ptr исчезает, и у нас есть свисающая ссылка в __range,


Вы можете исправить это с помощью небольшого помощника:

template<class Ptr>
struct range_ptr_t {
Ptr p;
auto begin() const {
using std::begin;
return begin(*p);
}
auto end() const {
using std::end;
return end(*p);
}
};
template<class Ptr>
range_ptr_t<std::decay_t<Ptr>> range_ptr( Ptr&& ptr ) {
return {std::forward<Ptr>(ptr)};
}

теперь мы делаем:

for (int i : range_ptr(evens))
std::cout << i << "\n";

и у нас больше нет уникального птр, умирающего на нас.

Это может быть хорошей идеей, чтобы продлить срок службы range_expression к телу for(:) Цикл, так как эта проблема приводит к другим проблемам (например, при подключении адаптеров диапазона), которые заканчиваются аналогичным раздражающим обходным путем.

Минимальный тестовый пример:

std::unique_ptr<std::vector<int>> foo() {
return std::make_unique<std::vector<int>>( std::vector<int>{ 1, 2, 3} );
}int main() {
for (int x : range_ptr(foo())) {
std::cout << x << '\n';
}
}
3

Другие решения

Ваш код демонстрирует неопределенное поведение. gcc, msvc и clang ведут себя одинаково; деструктор итераторов запускается до того, как что-либо будет выведено.

В этом случае цикл for, основанный на диапазоне, можно рассматривать как удобный способ кэширования вызова функции, поэтому ваш код эквивалентен* ([Stmt.ranged]):

auto&& range = *foo.iterator(1);
for (auto __begin = range.begin(), __end = range.end(); __begin!=__end; ++__begin){
int i = *__begin;
std::cout << i << "\n";
}

Разыменовывая уникальный_птр, range становится ссылкой на базовый Iterator, который затем сразу выходит за рамки.

* эти правила немного изменяются в C ++ 17, так что __begin а также __end не должен быть того же типа

3

По вопросам рекламы [email protected]