проблема
Как я понимаю, когда 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
for(range_declaration:range_expression)
выражение эквивалентно (в C ++ 11 а также C ++ 14) к:
{
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';
}
}
Ваш код демонстрирует неопределенное поведение. 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
не должен быть того же типа