关于c ++:T&&在c++11中的含义

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

我一直在研究C++ 11的一些新特性,我注意到其中一个是声明变量的双字节,比如EDCOX1(0)。

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

这到底是什么意思?

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


它声明一个右值引用(标准建议文档)。

这里是对右值引用的介绍。

下面是微软标准库开发人员对右值引用的深入了解。

CAUTION: the linked article on MSDN ("Rvalue References: C++0x Features in VC10, Part 2") is a very clear introduction to Rvalue references, but makes statements about Rvalue references that were once true in the draft C++11 standard, but are not true for the final one! Specifically, it says at various points that rvalue references can bind to lvalues, which was once true, but was changed.(e.g. int x; int &&rrx = x; no longer compiles in GCC) – drewbarbs Jul 13 '14 at 16:12

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

1
T&& r = T();

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

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

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

1
2
3
4
5
6
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中,我们可以重载一个移动构造函数:

1
2
3
4
5
6
7
foo(foo&& other)
{
   this->length = other.length;
   this->ptr = other.ptr;
   other.length = 0;
   other.ptr = nullptr;
}

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

move构造函数将用于临时引用和使用std::move函数(它只执行转换)显式转换为右值引用的非常量左值引用。以下代码同时调用f1f2的move构造函数:

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

完美的转发。值引用允许我们正确地转发模板化函数的参数。以该工厂功能为例:

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

如果我们称为factory(5),那么这个参数将被推断为int&,即使foo的构造函数采用int,它也不会绑定到文字5。好吧,我们可以使用A1 const&,但是如果foo采用非常量引用的构造函数参数呢?为了实现真正的通用工厂功能,我们必须在A1&A1 const&上重载工厂。如果工厂采用1个参数类型,这可能很好,但每个附加的参数类型都会将所需的重载集乘以2。这很快就无法挽回了。

rvalue引用通过允许标准库定义一个可以正确转发lvalue/rvalue引用的std::forward函数来解决这个问题。有关std::forward如何工作的更多信息,请参阅这个极好的答案。

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

1
2
3
4
5
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的构造函数。改进的工厂功能可以工作,因为有一个特殊规则:

When the function parameter type is of
the form T&& where T is a template
parameter, and the function argument
is an lvalue of type A, the type A& is
used for template argument deduction.

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

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

重要的右值引用属性:

  • 对于重载解析,lvalue更喜欢绑定到lvalue引用,rvalue更喜欢绑定到rvalue引用。因此,为什么临时性更喜欢调用移动构造函数/移动分配运算符而不是复制构造函数/分配运算符。
  • 右值引用将隐式绑定到右值和作为隐式转换结果的临时值。也就是说,float f = 0f; int&& i = f;的格式很好,因为float可以隐式地转换为int;引用将是转换的结果的临时引用。
  • 命名的右值引用是左值。未命名的右值引用是右值。这对于理解为什么在:foo&& r = foo(); foo f = std::move(r);中需要std::move调用很重要。


它表示一个右值引用。除非另有明确生成,否则右值引用将只绑定到临时对象。它们用于使对象在某些情况下更高效,并提供一种称为完美转发的功能,这大大简化了模板代码。

在C++ 03中,你不能区分一个不可变的LValk的拷贝和一个rValk。

1
2
3
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中,情况并非如此。

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

考虑这些构造函数背后的实现。在第一种情况下,字符串必须执行复制以保留值语义,这涉及到新的堆分配。但是,在第二种情况下,我们提前知道传递给构造函数的对象将立即被销毁,并且不必保持原样。在这种情况下,我们可以有效地交换内部指针,而根本不执行任何复制,这实际上更有效。移动语义有利于任何对内部引用资源进行昂贵或禁止复制的类。考虑EDOCX1 0的情况——现在我们的类可以区分临时和非临时性,我们可以使移动语义正确地工作,使得EDCOX1 1不能被复制但可以被移动,这意味着EDCOX1〔0〕可以合法存储在标准容器、排序等中,而C++ 03的EDCOX1引用3不能。

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

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

无法回忆起C++ 03关于这一点的说法,但是在C++ 0x中,处理rValk引用时的结果类型是至关重要的。对类型t的右值引用(其中t是引用类型)将成为类型t的引用。

1
2
3
4
(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和unconst的组合手动重载。在C++ 0x中,它只是一个重载。与可变模板相结合,可以实现完美的转发。

1
2
3
4
5
6
7
8
9
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(bref))
        return std::forward<A>(aref);
    else
        return std::forward(bref);
}

我取消了返回类型的扣除,因为我记不起它是如何立即完成的,但是Min可以接受lvalues、rvalues、const lvalues的任何组合。


当与类型演绎(例如用于完美转发)一起使用时,T&&的术语通常称为转发引用。术语"通用参考"是由ScottMeyers在本文中创造的,但后来被修改了。

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

例子有:

1
2
3
4
5
6
7
8
9
10
11
12
// template
template<class T> foo(T&& t) { ... }

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

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

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

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


右值引用是一种行为类似于普通引用X&;的类型,但有几个例外。最重要的一点是,在函数重载解决方案方面,lvalues更喜欢旧式lvalue引用,而rvalues更喜欢新的rvalue引用:

1
2
3
4
5
6
7
8
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完成了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
 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
"
;
  }

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

构造函数和赋值运算符已被采用右值引用的版本重载。右值引用允许函数在编译时(通过重载解析)在条件"是否对左值或右值调用?"。这允许我们在上面创建更高效的构造函数和赋值运算符,以便移动资源,而不是复制它们。

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

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

一个易于理解的实用示例是类模板std::unique_ptr。由于唯一指针保持其基础原始指针的独占所有权,因此无法复制唯一指针。这将违背它们的专属所有权不变性。所以它们没有复制构造函数。但它们确实有移动构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:
"
;

for(auto i = 0; i< 10; ++i) {
   cout << new_owner[i] <<",";
}

output:
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,

static_cast&&>(ptr)通常使用std::move来完成。

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

一个很好的文章解释所有这些和更多(如rValm如何允许完美的转发,这意味着什么)有很多很好的例子是Thomas Becker的C++ RValk引用解释。这篇文章很大程度上依赖于他的文章。

简短的介绍是stroutrup等人对右值引用的简要介绍。