首页 文章

T &&(双&符号)在C 11中意味着什么?

提问于
浏览
648

我注意到在声明变量时是双&符号,如 T&& var .

首先,这只野兽叫什么?我希望谷歌允许我们搜索这样的标点符号 .

究竟是什么意思?

乍一看,它似乎是一个双引用(如C风格的双指针 T** var ),但我很难想到一个用例 .

4 回答

  • 573

    它声明了一个rvalue reference(标准提案文档) .

    这是rvalue references的介绍 .

    这里是's a fantastic in-depth look at rvalue references by one of Microsoft'的标准库developers . (但在阅读本文之前,请参阅本答案后面的评论中的注意事项 . )

    C 03引用(现在称为C 11中的左值引用)之间的最大区别在于它可以像临时一样绑定到rvalue而不必是const . 因此,此语法现在是合法的:

    T&& r = T();
    

    右值参考主要提供以下内容:

    Move semantics . 现在可以定义移动构造函数和移动赋值运算符,它采用右值引用而不是通常的const-lvalue引用 . 移动的功能类似于副本,除非它没有义务保持源不变;实际上,它通常会修改源,使其不再拥有移动的资源 . 这对于消除无关副本非常有用,尤其是在标准库实现中 .

    例如,复制构造函数可能如下所示:

    foo(foo const& other)
    {
        this->length = other.length;
        this->ptr = new int[other.length];
        copy(other.ptr, other.ptr + other.length, this->ptr);
    }
    

    如果这个构造函数是临时传递的,那么副本将是不必要的,因为我们知道临时将被销毁;为什么不利用临时已分配的资源?在C 03中,没有办法阻止复制,因为我们无法确定我们是否通过了临时复制 . 在C 11中,我们可以重载一个移动构造函数:

    foo(foo&& other)
    {
       this->length = other.length;
       this->ptr = other.ptr;
       other.length = 0;
       other.ptr = nullptr;
    }
    

    请注意这里的重大区别:移动构造函数实际上修改了它的参数 . 这将有效地将临时“移动”到正在构造的对象中,从而消除不必要的副本 .

    移动构造函数将用于temporaries和非const左值引用,这些引用使用 std::move 函数显式转换为rvalue引用(它只执行转换) . 以下代码都调用 f1f2 的移动构造函数:

    foo f1((foo())); // Move a temporary into f1; temporary becomes "empty"
    foo f2 = std::move(f1); // Move f1 into f2; f1 is now "empty"
    

    Perfect forwarding . 右值引用允许我们正确转发模板化函数的参数 . 以此工厂功能为例:

    template <typename T, typename A1>
    std::unique_ptr<T> factory(A1& a1)
    {
        return std::unique_ptr<T>(new T(a1));
    }
    

    如果我们调用 factory<foo>(5) ,则该参数将被推断为 int& ,它不会绑定到文字5,即使 foo 的构造函数采用 int . 好吧,我们可以使用 A1 const& ,但是如果 foo 通过非const引用获取构造函数参数呢?要创建一个真正通用的工厂函数,我们必须在 A1&A1 const& 上重载工厂 . 如果工厂采用1参数类型,那可能没问题,但是每个额外的参数类型会将必要的重载乘以2 . 这很快就无法维护 .

    右值引用通过允许标准库定义可以正确转发左值/右值引用的 std::forward 函数来解决此问题 . 有关 std::forward 如何工作的更多信息,请参阅this excellent answer .

    这使我们能够像这样定义工厂函数:

    template <typename T, typename A1>
    std::unique_ptr<T> factory(A1&& a1)
    {
        return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
    }
    

    现在,当传递给 T 的构造函数时,参数的rvalue / lvalue-ness被保留 . 这意味着如果使用rvalue调用工厂,则使用rvalue调用 T 的构造函数 . 如果使用左值调用工厂,则使用左值调用 T 的构造函数 . 由于一个特殊规则,改进的工厂功能起作用:

    当函数参数类型的形式为T &&,其中T是模板参数,并且函数参数是类型A的左值,类型A&用于模板参数推导 .

    因此,我们可以像这样使用工厂:

    auto p1 = factory<foo>(foo()); // calls foo(foo&&)
    auto p2 = factory<foo>(*p1);   // calls foo(foo const&)
    

    Important rvalue reference properties

    • 对于重载解析, lvalues prefer binding to lvalue references and rvalues prefer binding to rvalue references . 因此,为什么临时工具更喜欢在复制构造函数/赋值运算符上调用移动构造函数/移动赋值运算符 .

    • rvalue references will implicitly bind to rvalues and to temporaries that are the result of an implicit conversion . 即 float f = 0f; int&& i = f; 格式正确,因为float可以隐式转换为int;引用将是转换结果的临时值 .

    • Named rvalue references are lvalues. Unnamed rvalue references are rvalues. 这对于理解为什么需要 std::move 调用非常重要: foo&& r = foo(); foo f = std::move(r);

  • 19

    它表示右值参考 . 除非另外明确生成,否则Rvalue引用仅绑定到临时对象 . 它们用于在某些情况下使对象更有效,并提供称为完美转发的工具,这极大地简化了模板代码 .

    在C 03中,您无法区分不可变左值的副本和右值 .

    std::string s;
    std::string another(s);           // calls std::string(const std::string&);
    std::string more(std::string(s)); // calls std::string(const std::string&);
    

    在C 0x中,情况并非如此 .

    std::string s;
    std::string another(s);           // calls std::string(const std::string&);
    std::string more(std::string(s)); // calls std::string(std::string&&);
    

    考虑这些构造函数背后的实现 . 在第一种情况下,字符串必须执行复制以保留值语义,这涉及新的堆分配 . 但是,在第二种情况下,我们事先知道传递给我们的构造函数的对象是立即销毁的,并且它不必保持不变 . 在这种情况下,我们可以有效地交换内部指针而不执行任何复制,这实际上更有效 . 移动语义有益于任何具有昂贵或禁止复制内部引用资源的类 . 考虑 std::unique_ptr 的情况 - 现在我们的类可以区分临时和非临时,我们可以使移动语义正常工作,以便 unique_ptr 无法复制但可以移动,这意味着 std::unique_ptr 可以合法地存储在标准容器中,排序等,而C 03的 std::auto_ptr 不能 .

    现在我们考虑另一种使用rvalue引用 - 完美转发 . 考虑绑定对引用的引用的问题 .

    std::string s;
    std::string& ref = s;
    (std::string&)& anotherref = ref; // usually expressed via template
    

    不记得C 03对此的描述,但在C 0x中,处理右值参考时的结果类型是至关重要的 . 对类型T的右值引用(其中T是引用类型)成为类型T的引用 .

    (std::string&)&& ref // ref is std::string&
    (const std::string&)&& ref // ref is const std::string&
    (std::string&&)&& ref // ref is std::string&&
    (const std::string&&)&& ref // ref is const std::string&&
    

    考虑最简单的模板函数 - min和max . 在C 03中,您必须手动重载const和非const的所有四种组合 . 在C 0x中,它只是一个重载 . 结合可变参数模板,可实现完美转发 .

    template<typename A, typename B> auto min(A&& aref, B&& bref) {
        // for example, if you pass a const std::string& as first argument,
        // then A becomes const std::string& and by extension, aref becomes
        // const std::string&, completely maintaining it's type information.
        if (std::forward<A>(aref) < std::forward<B>(bref))
            return std::forward<A>(aref);
        else
            return std::forward<B>(bref);
    }
    

    我没有留下返回类型的推论,因为我无法回想起它是如何随意完成的,但是min可以接受左值,右值,常量值的任意组合 .

  • 76

    T&& when used with type deduction (例如用于完美转发)的术语通俗地称为 forwarding reference . "universal reference"这个词是由Scott Meyers创造的in this article,但后来被改变了 .

    那是因为它可能是r值或l值 .

    例子是:

    // template
    template<class T> foo(T&& t) { ... }
    
    // auto
    auto&& t = ...;
    
    // typedef
    typedef ... T;
    T&& t = ...;
    
    // decltype
    decltype(...)&& t = ...;
    

    可以在以下答案中找到更多讨论:Syntax for universal references

  • 8

    右值引用是一种与普通引用X&非常相似的类型,但有一些例外 . 最重要的一点是,当涉及函数重载解析时,左值更喜欢旧式左值引用,而右值更喜欢新的右值引用:

    void foo(X& x);  // lvalue reference overload
    void foo(X&& x); // rvalue reference overload
    
    X x;
    X foobar();
    
    foo(x);        // argument is lvalue: calls foo(X&)
    foo(foobar()); // argument is rvalue: calls foo(X&&)
    

    那么什么是右值?任何不是左值的东西 . 左值是一个表达式,它引用一个内存位置,并允许我们通过&运算符获取该内存位置的地址 .

    首先通过一个例子更容易理解rvalues的成就:

    class Sample {
      int *ptr; // large block of memory
      int size;
     public:
      Sample(int sz=0) : ptr{sz != 0 ? new int[sz] : nullptr}, size{sz} 
      {}
      // copy constructor that takes lvalue 
      Sample(const Sample& s) : ptr{s.size != 0 ? new int[s.size] :\
          nullptr}, size{s.size}
      {
         std::cout << "copy constructor called on lvalue\n";
      }
    
      // move constructor that take rvalue
      Sample(Sample&& s) 
      {  // steal s's resources
         ptr = s.ptr;
         size = s.size;        
         s.ptr = nullptr; // destructive write
         s.size = 0;
         cout << "Move constructor called on rvalue." << std::endl;
      }    
      // normal copy assignment operator taking lvalue
      Sample& operator=(const Sample& s)
      {
       if(this != &s) {
          delete [] ptr; // free current pointer
          ptr = new int[s.size]; 
          size = s.size; 
        }
        cout << "Copy Assignment called on lvalue." << std::endl;
        return *this;
      }    
     // overloaded move assignment operator taking rvalue
     Sample& operator=(Sample&& lhs)
     {
       if(this != &s) {
          delete [] ptr; //don't let ptr be orphaned 
          ptr = lhs.ptr;   //but now "steal" lhs, don't clone it.
          size = lhs.size; 
          lhs.ptr = nullptr; // lhs's new "stolen" state
          lhs.size = 0;
       }
       cout << "Move Assignment called on rvalue" << std::endl;
       return *this;
     }
    //...snip
    };
    

    构造函数和赋值运算符已经过载了带有右值引用的版本 . Rvalue引用允许函数在编译时(通过重载解析)在"Am I being called on an lvalue or an rvalue?"条件下进行分支 . This allowed us to create more efficient constructor and assignment operators above that move resources rather copy them.

    编译器在编译时自动分支(取决于是否为左值或右值调用它),选择是否应调用移动构造函数或移动赋值运算符 .

    总结:rvalue引用允许移动语义(和完美转发,在下面的文章链接中讨论) .

    一个易于理解的实用示例是类模板 std::unique_ptr . 由于unique_ptr维护其底层原始指针的独占所有权,因此复制unique_ptr 's can't . 这将违反他们对独家所有权的不变性 . 所以他们没有复制构造函数 . 但他们确实有移动构造函数:

    template<class T> class unique_ptr {
      //...snip
     unique_ptr(unique_ptr&& __u) noexcept; // move constructor
    };
    
     std::unique_ptr<int[] pt1{new int[10]};  
     std::unique_ptr<int[]> ptr2{ptr1};// compile error: no copy ctor.  
    
     // So we must first cast ptr1 to an rvalue 
     std::unique_ptr<int[]> ptr2{std::move(ptr1)};  
    
    std::unique_ptr<int[]> TakeOwnershipAndAlter(std::unique_ptr<int[]> param,\
     int size)      
    {
      for (auto i = 0; i < size; ++i) {
         param[i] += 10;
      }
      return param; // implicitly calls unique_ptr(unique_ptr&&)
    }
    
    // Now use function     
    unique_ptr<int[]> ptr{new int[10]};
    
    // first cast ptr from lvalue to rvalue
    unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(\
               static_cast<unique_ptr<int[]>&&>(ptr), 10);
    
    cout << "output:\n";
    
    for(auto i = 0; i< 10; ++i) {
       cout << new_owner[i] << ", ";
    }
    
    output:
    10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
    

    static_cast<unique_ptr<int[]>&&>(ptr) 通常使用 std::move 完成

    // first cast ptr from lvalue to rvalue
    unique_ptr<int[]> new_owner = TakeOwnershipAndAlter(std::move(ptr),0);
    

    一篇优秀的文章解释了所有这些以及更多(如rvalues如何允许完美转发以及这意味着什么)以及很多很好的例子,是Thomas Becker的C++ Rvalue References Explained . 这篇文章很大程度上依赖于他的文章 .

    Stroutrup等人的简短介绍是A Brief Introduction to Rvalue References . 人

相关问题