T&&(双&符号)在C++11中是什么意思?

What does T&& (double ampersand) mean in C++11?

提问人:paxdiablo 提问时间:3/30/2011 最后编辑:paxdiablo 更新时间:5/19/2021 访问量:334928

问:

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

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

这究竟意味着什么?

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

C ++11 右值引用 C++-FAQ 完美转发

评论

3赞 fredoverflow 3/30/2011
关于移动语义的相关问题
0赞 Daniel 8/23/2011
这里有一个非常好的、易于理解的回答,stackoverflow.com/questions/7153991/ 类似的问题......
6赞 sergiol 8/14/2015
我在 Google 中搜索“c++ two &ersands parameter”时,在顶部有三个 stackoverflow 问题,您的问题是第一个。因此,如果您可以拼写出“两个 & 符号参数”,您甚至不需要为此使用标点符号。
0赞 Destructor 2/4/2016
什么是移动语义的可能重复?
0赞 Millie Smith 2/26/2016
@sergiol 现在你没有了,但是当 2011 年初被问到这个问题时,“两个 & 符号参数”可能不起作用。

答:

841赞 28 revs, 11 users 88%Peter Huene #1

它声明了一个右值引用(标准提案文档)。

下面是对右值引用的介绍。

以下是 Microsoft 标准库开发人员之一对右值引用的精彩深入介绍。

注意:MSDN 上的链接文章(“右值引用:VC10 中的 C++x 功能,第 2 部分”)是对右值引用的非常清晰的介绍,但对右值引用的陈述在 C++11 标准草案中曾经是正确的,但对于最终标准来说却不是真的!具体来说,它在不同的地方说右值引用可以绑定到左值,这曾经是真的,但被改变了。(例如 int x; int &&rrx = x; 不再在 GCC 中编译) – drewbarbs Jul 13 '14 at 16:12

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

T&& r = T();

右值引用主要提供以下内容:

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

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

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

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

move 构造函数将用于临时引用和非常量左值引用,这些引用使用函数显式转换为右值引用(它只是执行转换)。以下代码都调用了 和 的移动构造函数:std::movef1f2

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

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

右值引用通过允许标准库定义一个可以正确转发左值/右值引用的函数来解决此问题。有关工作原理的更多信息,请参阅此出色答案std::forwardstd::forward

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

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

现在,参数的 rvalue/lvalue-ness 在传递给 的构造函数时被保留。这意味着,如果使用 rvalue 调用 factory,则使用 rvalue 调用 的构造函数。如果用左值调用工厂,则用左值调用 的构造函数。改进后的工厂功能之所以有效,是因为有一条特殊规则:TTT

当函数参数类型为 表单 where 是模板 参数和函数参数 是 类型的左值,类型为 用于模板参数推导。T&&TAA&

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

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

重要的右值引用属性

  • 对于重载解析,左值首选绑定到左值引用,而右值首选绑定到右值引用。因此,为什么临时人员更喜欢调用移动构造函数/移动赋值运算符而不是复制构造函数/赋值运算符。
  • 右值引用将隐式绑定到右值和作为隐式转换结果的临时值。即 格式良好,因为 float 可以隐式转换为 int;引用将指向作为转换结果的临时数据。float f = 0f; int&& i = f;
  • 命名右值引用是左值。未命名的右值引用是右值。这对于理解为什么在以下情况下需要调用非常重要:std::movefoo&& r = foo(); foo f = std::move(r);

评论

89赞 legends2k 5/30/2013
+1 代表 ;在不知道这一点的情况下,我一直在努力理解为什么人们在移动 ctors 等方面做了很长时间。Named rvalue references are lvalues. Unnamed rvalue references are rvalues.T &&t; std::move(t);
0赞 Peter Huene 8/1/2013
@MaximYegorushkin:在这个例子中,r 绑定到一个纯 rvalue(临时的),因此临时值应该延长其生存期范围,不是吗?
0赞 Maxim Egorushkin 8/1/2013
@PeterHuene收回这一点,r 值引用确实延长了临时的生存期。
34赞 drewbarbs 7/14/2014
注意:MSDN 上的链接文章(“右值引用:VC10 中的 C++x 功能,第 2 部分”)是对右值引用的非常清晰的介绍,对右值引用的陈述在 C++11 标准草案中曾经是正确的,但对于最终标准来说却不是真的!具体来说,它在不同的地方说右值引用可以绑定到左值,这曾经是真的,但被改变了。(例如 不再在 GCC 中编译)int x; int &&rrx = x;
4赞 Olshansky 10/27/2020
为了更好地理解,有人可以解释以下陈述是否不正确吗 1. 可以被认为是临时的,其寿命没有保证。2.在示波器的持续时间内延长返回的寿命。3. 这些是否等价:和?rvaluesfoo&& r = foo()foo()foo&& rconst foo& r
101赞 Puppy 3/30/2011 #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&&);

考虑这些构造函数背后的实现。在第一种情况下,字符串必须执行复制以保留值语义,这涉及新的堆分配。但是,在第二种情况下,我们事先知道传递给构造函数的对象会立即销毁,并且不必保持不变。在这种情况下,我们可以有效地交换内部指针,根本不执行任何复制,这要高效得多。移动语义使任何具有昂贵或禁止复制内部引用资源的类受益。考虑一下这样的情况 - 现在我们的类可以区分临时和非临时,我们可以使移动语义正常工作,以便不能复制但可以移动,这意味着可以合法地存储在标准容器中,排序等,而 C++03 不能。std::unique_ptrunique_ptrstd::unique_ptrstd::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 中,您必须手动重载常量和非常量的所有四种组合。在 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 可以接受 lvalues、rvalues、const lvalues 的任意组合。

评论

2赞 Yankes 7/4/2013
你为什么使用?而且我认为当你尝试前进和.最好删除一种类型的表单模板。std::forward<A>(aref) < std::forward<B>(bref)int&float&
29赞 mmocny 1/17/2013 #3

与类型推导(例如完美转发)一起使用时的术语通俗地称为转发参考。“通用参考”一词是由斯科特·迈耶斯(Scott Meyers)在本文中创造的,但后来发生了变化。T&&

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

例如:

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

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

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

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

更多讨论可以在以下答案中找到:通用引用的语法

23赞 kurt krueckeberg 11/3/2016 #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&&)

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

首先,通过一个示例,几乎可以更容易地理解 rvalues 的完成:

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

托马斯·贝克尔(Thomas Becker)的C++《右值参考文献解释》(Rvalue References Explained)是一篇很好的文章,其中有很多很好的例子,解释了所有这些以及更多内容(例如右值如何允许完美的转发以及这意味着什么)。这篇文章很大程度上依赖于他的文章。

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

评论

1赞 K.Karamazen 12/29/2019
难道不是这样复制构造函数也需要复制内容吗?“复制赋值运算符”的相同问题。Sample(const Sample& s)
1赞 kurt krueckeberg 12/30/2019
是的,你是对的。我无法复制内存。复制构造函数和复制赋值运算符都应该在测试大小 != 0 后执行 memcpy(ptr, s.ptr, size)。如果 size != 0,则默认构造函数应执行 memset(ptr,0, size)。
0赞 K.Karamazen 12/31/2019
好的,谢谢。因此,可以删除此注释和前两条注释,因为该问题也已在答案中得到纠正。