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

时间:2017-10-26 18:49:28

标签: c++ c++11 scope iterator ranged-loops

问题

据我了解,当从函数返回std::unique_ptr到rvalue时,它的生命周期应包含消耗该rvalue的语句。但是使用gcc 6.4.1进行编译时,Foo::iterator()的返回值超出了函数crashing_version()中C ++ 11 foreach语句的 start 之前的范围。如下面的输出所示,在计算包含表达式之后立即调用析构函数。这是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 个答案:

答案 0 :(得分: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";
}

通过取消引用unique_ptr,range成为对基础Iterator的引用,然后立即超出范围。

*这些规则在C ++ 17中稍有变化,因此__begin__end不需要是同一类型

答案 1 :(得分:3)

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,变量以__开头仅作为exponsition存在。

我们替换:

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';
  }
}