简体   繁体   English

C ++ 11迭代器和返回的std :: unique_ptr的范围

[英]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 and ) to: 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
  }
} 

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_expressionfor(:)循环的主体可能是一个好主意,因为此问题会导致其他问题(例如,在链接范围适配器时),最终导致类似的烦人解决方法。

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM