C++11 中的 T&&(双 & 符号)是啥意思?

Posted

技术标签:

【中文标题】C++11 中的 T&&(双 & 符号)是啥意思?【英文标题】:What does T&& (double ampersand) mean in C++11?C++11 中的 T&&(双 & 符号)是什么意思? 【发布时间】:2011-07-25 18:57:37 【问题描述】:

我一直在研究 C++11 的一些新特性,我注意到其中一个是声明变量时的双 & 符号,例如 T&& var

首先,这个野兽叫什么?我希望 Google 允许我们搜索这样的标点符号。

具体是什么意思

乍一看,它似乎是一个双重引用(就像 C 风格的双指针 T** var),但我很难考虑它的用例。

【问题讨论】:

我已将此添加到 c++-faq 中,因为我相信它会在未来出现更多。 关于move semantics的相关问题 你可以用谷歌搜索这个,你只需要用引号括起来你的短语:google.com/#q="T%26%26" 现在你的问题是第一个命中。:) 这里有一个很好的、易于理解的对类似问题的回复***.com/questions/7153991/… 我在 Google 的顶部搜索“c++ 两个 & 符号参数”时遇到了三个 *** 问题,而你的问题是第一个。因此,如果您可以拼出“两个与号参数”,您甚至不需要使用标点符号。 【参考方案1】:

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

这里介绍一下右值references。

这里是微软标准库developers 对右值引用的深入了解。

注意: MSDN 上的链接文章(“Rvalue References: C++0x Features in VC10, Part 2”)是对 Rvalue 引用的非常清晰的介绍,但对右值引用在 C++11 标准草案中曾经是正确的,但在最终标准中不是正确的!具体来说,它在不同的点上说右值引用可以绑定到左值,这曾经是正确的,但被改变了。(例如 int x; int &&rrx = x; 不再在 GCC 中编译) – 2014-07-13 16:12:47

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

T&& r = T();

右值引用主要提供以下功能:

移动语义。现在可以定义一个移动构造函数和移动赋值运算符,它采用右值引用而不是通常的 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;

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

移动构造函数将用于临时对象和非常量左值引用,这些引用使用std::move 函数显式转换为右值引用(它只是执行转换)。以下代码均调用 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"

完美转发。右值引用允许我们正确地转发模板函数的参数。以这个工厂函数为例:

template <typename T, typename A1>
std::unique_ptr<T> factory(A1& a1)

    return std::unique_ptr<T>(new T(a1));

如果我们调用factory&lt;foo&gt;(5),参数将被推导出为int&amp;,它不会绑定到文字5,即使foo的构造函数采用int。好吧,我们可以改用A1 const&amp;,但是如果foo 通过非常量引用获取构造函数参数呢?要创建一个真正通用的工厂函数,我们必须在 A1&amp;A1 const&amp; 上重载工厂。如果 factory 采用 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 的构造函数时被保留。这意味着如果使用右值调用工厂,则使用右值调用T 的构造函数。如果使用左值调用工厂,则使用左值调用T 的构造函数。改进后的工厂函数之所以起作用,是因为有一个特殊规则:

当函数参数类型为 表单T&amp;&amp; 其中T 是一个模板 参数和函数参数 是A 类型的左值,A&amp; 类型是 用于模板参数推导。

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

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

重要的右值引用属性

对于重载决议,左值更喜欢绑定到左值引用,而右值更喜欢绑定到右值引用。因此,为什么临时工更喜欢调用移动构造函数/移动赋值运算符而不是复制构造函数/赋值运算符。 右值引用将隐式绑定到右值和作为隐式转换结果的临时变量。即 float f = 0f; int&amp;&amp; i = f; 格式正确,因为 float 可以隐式转换为 int;引用将指向作为转换结果的临时对象。 命名的右值引用是左值。未命名的右值引用是右值。 这对于理解std::move 调用在以下情况下的必要性很重要:foo&amp;&amp; r = foo(); foo f = std::move(r);

【讨论】:

+1 代表Named rvalue references are lvalues. Unnamed rvalue references are rvalues.;在不知道这一点的情况下,我一直在努力理解为什么人们会在很长一段时间内使用 T &amp;&amp;t; std::move(t);,等等。 @MaximYegorushkin:在那个例子中,r 绑定到一个纯右值(临时),因此临时应该有它的生命周期范围扩展,不是吗? @PeterHuene 我收回这一点,r 值引用确实延长了临时的生命周期。 CAUTION:MSDN上的链接文章(“Rvalue References: C++0x Features in VC10, Part 2”)很清楚在介绍右值引用时,but 对在 C++11 标准草案中曾经为真,但对于最后一个!具体来说,它在各个方面都说右值引用可以绑定到左值,这曾经是正确的,但是was changed.(例如,GCC 中的int x; int &amp;&amp;rrx = x;no longer compiles) 为了更好地理解,如果以下陈述不正确,有人可以解释一下 1. rvalues 可以被认为是无法保证生命周期的临时人员。 2.foo&amp;&amp; r = foo()foo() 返回的生命周期延长到作用域的持续时间。 3. 这些是否等效:foo&amp;&amp; rconst foo&amp; r【参考方案2】:

它表示一个右值引用。除非以其他方式显式生成,否则右值引用只会绑定到临时对象。它们用于在某些情况下使对象更加高效,并提供一种称为完美转发的工具,从而大大简化模板代码。

在 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 则不能。

现在我们考虑右值引用的另一种用法——完美转发。考虑将引用绑定到引用的问题。

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&&

考虑最简单的模板函数——最小值和最大值。在 C++03 中,您必须手动重载 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 可以接受左值、右值、const 左值的任意组合。

【讨论】:

你为什么使用std::forward&lt;A&gt;(aref) &lt; std::forward&lt;B&gt;(bref)?当您尝试转发int&amp;float&amp; 时,我认为这个定义是不正确的。最好删除一种类型的表单模板。【参考方案3】:

T&amp;&amp; 的术语当与类型推导一起使用时(例如完美转发)通俗地称为转发引用。 “通用参考”一词由 Scott Meyers in this article 创造,但后来被更改。

那是因为它可能是右值或左值。

例如:

// template
template<class T> foo(T&& t)  ... 

// auto
auto&& t = ...;

// typedef
typedef ... T;
T&& t = ...;

// decltype
decltype(...)&& t = ...;

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

【讨论】:

【参考方案4】:

右值引用是一种行为与普通引用 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&&)

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

首先通过一个例子来理解右值的作用几乎更容易:

 #include <cstring>
 class Sample 
  int *ptr; // large block of memory
  int size;
 public:
  Sample(int sz=0) : ptrsz != 0 ? new int[sz] : nullptr, sizesz 
  
     if (ptr != nullptr) memset(ptr, 0, sz);
  
  // copy constructor that takes lvalue 
  Sample(const Sample& s) : ptrs.size != 0 ? new int[s.size] :\
      nullptr, sizes.size
  
     if (ptr != nullptr) memcpy(ptr, s.ptr, 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
      size = s.size;

      if (size != 0) 
        ptr = new int[s.size];
        memcpy(ptr, s.ptr, s.size);
       else 
         ptr = nullptr;
     
     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
;     

构造函数和赋值运算符已被采用右值引用的版本重载。右值引用允许函数在编译时(通过重载解析)在“我是在左值还是右值上被调用?”的条件下进行分支。 这使我们能够在上面创建更高效​​的构造函数和赋值运算符来移动资源而不是复制它们。

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

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

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

template<class T> class unique_ptr 
  //...snip
 unique_ptr(unique_ptr&& __u) noexcept; // move constructor
;

 std::unique_ptr<int[] pt1new int[10];  
 std::unique_ptr<int[]> ptr2ptr1;// compile error: no copy ctor.  

 // So we must first cast ptr1 to an rvalue 
 std::unique_ptr<int[]> ptr2std::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[]> ptrnew 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&lt;unique_ptr&lt;int[]&gt;&amp;&amp;&gt;(ptr) 通常使用 std::move

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

Thomas Becker 的C++ Rvalue References Explained 是一篇很好的文章,解释了所有这些以及更多内容(例如右值如何实现完美转发以及这意味着什么),并提供了很多很好的示例。这篇文章在很大程度上依赖于他的文章。

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

【讨论】:

是不是拷贝构造函数Sample(const Sample&amp; s)也需要拷贝内容? '复制赋值运算符'的相同问题。 是的,你是对的。我无法复制内存。复制构造函数和复制赋值运算符都应该在测试 size != 0 后执行 memcpy(ptr, s.ptr, size)。如果 size != 0,默认构造函数应该执行 memset(ptr,0, size)。跨度> 好的,谢谢。因此这个评论和前两个cmet可以去掉,因为问题也已经在答案中得到纠正。

以上是关于C++11 中的 T&&(双 & 符号)是啥意思?的主要内容,如果未能解决你的问题,请参考以下文章

std::move和std::forward

c语言声明08

c++11 用 std::swap vs operator=(T&&) 清除容器

[C++11]forward完美转发

C++11新特性之move与forward

在 C++ 中加速双精度绝对值