[英]C++11 iterator and the scope of a returned std::unique_ptr
Problem 问题
As I understand it, when a std::unique_ptr
is returned from a function into an rvalue, its lifetime should encompass the statement that consumes that rvalue. 据我了解,当
std::unique_ptr
从函数返回到右值时,其生存期应包含消耗该右值的语句。 But compiling with gcc 6.4.1, the return value from Foo::iterator()
goes out of scope before the start of the C++11 foreach statement in function crashing_version()
. 但是使用gcc 6.4.1进行编译时,
Foo::iterator()
的返回值超出了函数crashing_version()
C ++ 11 foreach语句开始之前的范围。 As shown in the output below, the destructor is called just after the containing expression is evaluated. 如下面的输出所示,在评估包含表达式之后,将调用析构函数。 Is this a bug in gcc, or bad programming practice?
这是gcc中的错误,还是不良的编程习惯?
Use Case 用例
The goal of this pattern is to make iteration available without exposing the private vectors. 该模式的目标是在不暴露私有向量的情况下使迭代可用。 This seems to require some object like
Foo::Iterator
because there are two separate lists to iterate. 这似乎需要某些对象,例如
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;
}
Program output: 程序输出:
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
Your code exhibits undefined behavior. 您的代码表现出未定义的行为。 gcc, msvc, and clang all behave the same;
gcc,msvc和clang的行为都相同; the iterators' destructor runs before anything is output.
迭代器的析构函数在任何输出之前运行。
The range-based for loop in this case can be treated as a convenient way of caching a function call, so your code is equivalently this * ([stmt.ranged]): 在这种情况下,基于范围的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";
}
By de-referencing the unique_ptr, range
becomes a reference to the underlying Iterator
, which then immediately goes out of scope. 通过取消引用unique_ptr,
range
成为对基础Iterator
的引用,然后该Iterator
立即超出范围。
*these rules change slightly in C++17 so that __begin
and __end
don't need to be the same type *这些规则在C ++ 17中略有变化,因此
__begin
和__end
不必是同一类型
A for(range_declaration:range_expression)
expression is equivalent (in c++11 and c++14 ) to: 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
}
}
source , with variables starting with __
existing only as exponsition. source ,以
__
开头的变量仅作为指数存在。
We substitute: 我们替代:
for (int i : *evens)
std::cout << i << "\n";
and we get: 我们得到:
{
auto && __range = *evens;
for (
auto __begin = begin_expr, __end = end_expr;
__begin != __end;
++__begin)
{
int i = *__begin;
std::cout << i << "\n";
}
}
we can now clearly see your bug. 现在,我们可以清楚地看到您的错误了。 Your unique ptr lasts as long as the
__range
line, but after dereferencing the unique ptr goes away, and we have a dangling reference in __range
. 您唯一的ptr的持续时间与
__range
线一样长,但是在取消对唯一ptr的引用后,该__range
消失了,并且我们在__range
有一个悬空的引用。
You can fix this with a little helper: 您可以使用一些小助手来解决此问题:
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)};
}
now we do: 现在我们做:
for (int i : range_ptr(evens))
std::cout << i << "\n";
and we no longer have the unique ptr dying on us. 而且我们不再有唯一的垂死之路。
It might be a good idea to extend the lifetime of range_expression
to the body of the for(:)
loop, as this problem leads to other issues (like when chaining range adapters) that end up with similarly annoying workarounds. 将
range_expression
的生存期range_expression
到for(:)
循环的主体可能是一个好主意,因为此问题会导致其他问题(例如,在链接范围适配器时),最终导致类似的烦人解决方法。
Minimal testcase: 最小的测试用例:
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';
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.