C++11 迭代器和返回的范围 std::unique_ptr
C++11 iterator and the scope of a returned std::unique_ptr
问题
据我了解,当 std::unique_ptr
从函数 return 编辑为右值时,其生命周期应包含使用该右值的语句。但是使用 gcc 6.4.1 编译时,Foo::iterator()
中的 return 值在函数 [= 中的 C++11 foreach 语句的 start 之前超出范围15=]。如下面的输出所示,析构函数在包含表达式被求值后立即被调用。这是 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
您的代码表现出未定义的行为。 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";
}
通过取消引用 unique_ptr,range
成为对基础 Iterator
的引用,然后立即超出范围。
*这些规则在 C++17 中略有变化,因此 __begin
和 __end
不需要是同一类型
A for(range_declaration:range_expression)
表达式等效于(在 c++11 and c++14 中):
{
auto && __range = range_expression ;
for (
auto __begin = begin_expr, __end = end_expr;
__begin != __end;
++__begin)
{
range_declaration = *__begin;
loop_statement
}
}
source,以 __
开头的变量仅作为表达式存在。
我们代入:
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";
我们不再有独特的 ptr 死在我们身上。
将 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';
}
}
问题
据我了解,当 std::unique_ptr
从函数 return 编辑为右值时,其生命周期应包含使用该右值的语句。但是使用 gcc 6.4.1 编译时,Foo::iterator()
中的 return 值在函数 [= 中的 C++11 foreach 语句的 start 之前超出范围15=]。如下面的输出所示,析构函数在包含表达式被求值后立即被调用。这是 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
您的代码表现出未定义的行为。 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";
}
通过取消引用 unique_ptr,range
成为对基础 Iterator
的引用,然后立即超出范围。
*这些规则在 C++17 中略有变化,因此 __begin
和 __end
不需要是同一类型
A for(range_declaration:range_expression)
表达式等效于(在 c++11 and c++14 中):
{
auto && __range = range_expression ;
for (
auto __begin = begin_expr, __end = end_expr;
__begin != __end;
++__begin)
{
range_declaration = *__begin;
loop_statement
}
}
source,以 __
开头的变量仅作为表达式存在。
我们代入:
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";
我们不再有独特的 ptr 死在我们身上。
将 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';
}
}