[英]What does T&& (double ampersand) mean in C++11?
我一直在研究 C++11 的一些新特性,我注意到的一个是声明变量时的双与号,比如T&& var
。
首先,这只野兽叫什么? 我希望谷歌允许我们搜索这样的标点符号。
究竟是什么意思?
乍一看,它似乎是一个双引用(如 C 风格的双指针T** var
),但我很难想到一个用例。
它声明了一个右值引用(标准提案文档)。
这是对右值引用的介绍。
这是 Microsoft 的标准库开发人员之一对右值引用的精彩深入研究。
注意: MSDN 上的链接文章(“Rvalue 引用:VC10 中的 C++0x 特性,第 2 部分”)是对 Rvalue 引用的非常清楚的介绍,但对 C++11 草案中曾经正确的 Rvalue 引用进行了陈述标准,但不是最后一个! 具体来说,它在各个方面都说右值引用可以绑定到左值,这曾经是正确的,但已更改。(例如 int x; int &&rrx = x; 不再在 GCC 中编译)–drewbarbs 2014 年 7 月 13 日 16:12
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
函数显式转换为右值引用(它只是执行转换)。 以下代码均调用f1
和f2
的移动构造函数:
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<foo>(5)
,参数将被推导出为int&
,它不会绑定到文字 5 ,即使foo
的构造函数采用int
。 好吧,我们可以改为使用A1 const&
,但是如果foo
通过非常量引用获取构造函数参数怎么办? 为了创建一个真正通用的工厂函数,我们必须在A1&
和A1 const&
上重载工厂。 如果工厂采用 1 个参数类型,那可能没问题,但是每个额外的参数类型都会将必要的重载集乘以 2。这很快就无法维护。
右值引用通过允许标准库定义可以正确转发左值/右值引用的std::forward
函数来解决这个问题。 有关std::forward
如何工作的更多信息,请参阅此优秀答案。
这使我们能够像这样定义工厂函数:
template <typename T, typename A1>
std::unique_ptr<T> factory(A1&& a1)
{
return std::unique_ptr<T>(new T(std::forward<A1>(a1)));
}
现在参数的右值/左值在传递给T
的构造函数时被保留。 这意味着如果用右值调用 factory,则用右值调用T
的构造函数。 如果用左值调用 factory,则用左值调用T
的构造函数。 改进后的工厂函数由于一条特殊规则而起作用:
当函数参数类型为
T&&
形式,其中T
是模板参数,并且函数参数是类型A
的左值时,类型A&
用于模板参数推导。
因此,我们可以像这样使用工厂:
auto p1 = factory<foo>(foo()); // calls foo(foo&&)
auto p2 = factory<foo>(*p1); // calls foo(foo const&)
重要的右值引用属性:
float f = 0f; int&& i = f;
float f = 0f; int&& i = f;
格式良好,因为 float 可以隐式转换为 int; 引用将是一个临时的,它是转换的结果。std::move
调用是必要的很重要: foo&& r = foo(); foo f = std::move(r);
foo&& r = foo(); foo f = std::move(r);
它表示一个右值引用。 除非明确生成,否则右值引用只会绑定到临时对象。 它们用于在某些情况下使对象更有效率,并提供一种称为完美转发的工具,这大大简化了模板代码。
在 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&&
考虑最简单的模板函数——min 和 max。 在 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 可以接受左值、右值、常量左值的任何组合。
右值引用是一种行为很像普通引用 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) : 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 。 阿尔
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.