【问题标题】:C++11 iterator and the scope of a returned std::unique_ptrC++11 迭代器和返回的 std::unique_ptr 的范围
【发布时间】:2018-04-08 06:35:02
【问题描述】:

问题

据我了解,当 std::unique_ptr 从函数返回到右值时,它的生命周期应该包含使用该右值的语句。但是使用 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

【问题讨论】:

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


    【解决方案1】:

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

    我们替换:

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

    【讨论】:

    • 不幸的是,由于这种行为的可观察性,在范围表达式内延长临时对象的生命周期将是一个突破性的变化。这使得它不如第一印象给人的好主意。
    • 我不完全明白为什么range_ptr_t 会保留在范围内。是不是因为__range 获取了range_ptr_t 的实际实例,因为它的类型直接兼容(隐藏的)迭代器规范?
    • @ByronHawkins __range 捕获range_ptr_t,因为range_expressionrange_ptr_t 类型的prvalue。 __range 捕获确切 range_expression 评估为的任何内容,并遵循auto&amp;&amp; 规则(在这种情况下,它使其成为绑定到prvalue 实例化临时的右值引用,然后引用生命周期扩展)。在您的情况下,它绑定到来自unique_ptr 的一元* 的返回值,这不是临时的,因此它的生命周期不会延长。现在,range_ptr_t 可以作为range_expression 工作,因为它有一个返回迭代器的beginend
    • @ByronHawkins 使用“迭代器”一词来指代概念或“范围”是一个坏主意。迭代器是 C++ 中定义的概念,不要为了不同的含义重复使用它。
    • @byron 的生命周期不仅仅是内存;它是副作用、各种资源、锁等。它是确定性清理。这确实意味着您必须始终跟踪谁拥有什么,智能容器/指针可以消除一些开销。
    【解决方案2】:

    您的代码表现出未定义的行为。 gcc、msvc 和 clang 的行为都相同;迭代器的析构函数在输出之前运行。

    在这种情况下,基于范围的 for 循环可以被视为缓存函数调用的一种便捷方式,因此您的代码等效为 this* ([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 不需要是同一类型

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-06
      • 2019-12-11
      • 1970-01-01
      • 2013-11-08
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多