右值引用(rvalue reference)
目录
1. 概述
2. 右值引用的语法格式
3. 移动语义(semantics)(语义:文字表达结合语境的含义)
4. 完备转交(Perfect forwarding)
5. 右值引用之属性
6. 总结
1. 概述
C++表达式有两个属性:类型和类型分类。虽然类型的更详细的分类不只2个,但在应用开发中,我们可以简单地认为类型分类只有左值和右值。这个分类有助于编译器确定表达式是否为值、函数、或对象。简单说,左值就是可以用程序取得其地址并进行访问的对象,而非左值即为右值。
C++11 添加了一种称为右值引用的新引用类型。右值引用是一种设计为仅用右值进行初始化的引用。左值引用和右值引用在语法和语义上相似,但它们遵循的规则略有不同。左值引用使用单个 & 符号创建,而右值引用使用两个 & 符号(&&)创建。
2. 右值引用的语法格式
右值引用类型id:
类型指定符序列 && 属性指定符序列(可选) 指针抽象声明符(可选)。
3. 移动语义(semantics)(语义:文字表达结合语境的含义)
右值引用支持移动语义(move semantics)的实现,这可以显著地提高应用程序的性能。移动语义使您能够编写将资源(例如动态分配的内存)从一个对象转移到另一个对象的代码。移动语义之所以有效,是因为它支持从临时对象(程序中他处无法引用的对象)转移资源。
要实现移动语义,通常需要为类提供一个移动构造函数,以及可选的移动赋值运算符 (operator=)。然后,移动源为右值的复制和赋值操作将自动利用移动语义。与默认复制构造函数不同,编译器不提供默认移动构造函数。有关如何编写和使用移动构造函数的详细信息,请参阅移动构造函数和移动赋值运算符。
您还可以重载普通函数和运算符以利用移动语义。例如,字符串类实现了使用移动语义的操作。请考虑以下连接多个字符串并打印结果的示例:
// string_concatenation.cpp
// compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;
int main()
{
string s = string("h") + "e" + "ll" + "o";
cout << s << endl;
}
在 Visual Studio 2010 之前,每次调用 operator+ 都会分配并返回一个新的临时字符串对象(右值)。operator+ 无法将一个字符串附加到另一个字符串,因为它不知道源字符串是左值还是右值。如果源字符串都是左值,则它们可能会在程序的其他位置被引用,因此不能修改。您可以修改 operator+ 以使用右值引用来获取右值,而右值引用不能在程序的其他位置被引用。通过此更改,operator+ 现在可以将一个字符串附加到另一个字符串。此更改显著减少了字符串类必须进行的动态内存分配次数。有关字符串类的更多信息,请参阅 basic_string 类。
当编译器无法使用返回值优化 (RVO) 或命名返回值优化 (NRVO) 时,移动语义也会有所帮助。在这些情况下,如果类型定义了移动构造函数,编译器会调用它。
为了更好地理解移动语义,请考虑将元素插入vector对象的示例。如果超出vector对象的容量,vector对象必须为其元素重新分配足够的内存,然后将每个元素复制到另一个内存位置,为插入的元素腾出空间。当插入操作复制元素时,它首先创建一个新元素。然后,它调用复制构造函数将数据从前一个元素复制到新元素。最后,它销毁前一个元素。移动语义使您能够直接移动对象,而无需进行昂贵的内存分配和复制操作。
为了利用向量示例中的移动语义,您可以编写移动构造函数将数据从一个对象移动到另一个对象。
4. 完备转交(Perfect forwarding)
完备转交减少了对重载函数的需求,并有助于避免转交问题。当你编写一个以引用为参数的泛型函数时,可能会发生转交问题。如果它将这些参数传递(或转交)给另一个函数,例如,如果它采用 const T& 类型的参数,则被调用的函数无法修改该参数的值。如果泛型函数采用 T& 类型的参数,则无法使用右值(如临时对象或整数文字)调用该函数。
考虑以下声明四种类型 W、X、Y 和 Z 的示例。每种类型的构造函数都采用不同的 const 和非 const 左值引用组合作为其参数。
struct W
{
W(int&, int&) {}
};
struct X
{
X(const int&, int&) {}
};
struct Y
{
Y(int&, const int&) {}
};
struct Z
{
Z(const int&, const int&) {}
};
假设您要编写一个生成对象的通用函数。以下示例显示了编写此函数的一种方法:
template <typename T, typename A1, typename A2>
T* factory(A1& a1, A2& a2)
{
return new T(a1, a2);
}
以下示例显示了对函数factory的有效调用:
int a = 4, b = 5;
W* pw = factory<W>(a, b);
但是,以下示例不包含对 factory 函数的有效调用。这是因为 factory 采用可修改的左值引用作为其参数,但它是使用右值进行调用的(译注:在C++11以前的版本中,传入右值的情况按左值引用方式处理,也就是说,如果按右值调用则更优):
Z* pz = factory<Z>(2, 2);
通常,为了解决这个问题,您必须为 A& 和 const A& 参数的每种组合创建一个factory函数的重载版本。右值引用使您能够编写一个factory函数版本,如以下示例所示:
template <typename T, typename A1, typename A2>
T* factory(A1&& a1, A2&& a2)
{
return new T(std::forward<A1>(a1), std::forward<A2>(a2));
}
此示例使用右值引用作为factory函数的参数。std::forward 函数的目的是将factory函数的参数转交给模板类的构造函数。
以下示例显示了使用修改后的factory函数创建 W、X、Y 和 Z 类实例的主函数。修改后的factory函数将其参数(左值或右值)转交给相应的类构造函数。
int main()
{
int a = 4, b = 5;
W* pw = factory<W>(a, b);
X* px = factory<X>(2, b);
Y* py = factory<Y>(a, 2);
Z* pz = factory<Z>(2, 2);
delete pw;
delete px;
delete py;
delete pz;
}
5. 右值引用之属性
您可以重载函数来获取左值引用和右值引用。
通过重载函数以接受 const 左值引用或右值引用,您可以编写区分不可修改对象(左值)和可修改临时对象的值(右值之一)的代码。您可以将对象传递给接受右值引用的函数(除非该对象被标记为 const)。以下展示了一个函数f,其中重载了函数以接受左值引用和右值引用。主函数分别使用左值引用和右值引用调用函数 f。
// reference-overload.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;
// A class that contains a memory resource.
class MemoryBlock
{
// TODO: Add resources for the class here.
};
void f(const MemoryBlock&)
{
cout << "In f(const MemoryBlock&). This version can't modify the parameter." << endl;
}
void f(MemoryBlock&&)
{
cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;
}
int main()
{
MemoryBlock block;
f(block);
f(MemoryBlock()); //临时对象是右值之一
}
这个例子输出如下:
In f(const MemoryBlock&). This version can't modify the parameter.
In f(MemoryBlock&&). This version can modify the parameter.
在此示例中,第一次调用 f 时将局部变量(左值)作为其参数传递。第二次调用 f 时将临时对象作为其参数传递。由于临时对象无法在程序中的其他地方引用,因此调用绑定到采用右值引用的 f 的重载版本,该版本可以自由修改对象。
编译器将命名的右值引用视为左值,将未命名的右值引用视为右值。
将右值引用作为参数的函数会将参数视为函数主体中的左值。编译器将命名的右值引用视为左值。这是因为命名对象可以被程序的多个部分引用。允许程序的多个部分修改或删除该对象的资源是危险的。例如,如果程序的多个部分尝试从同一对象转交资源,则只有第一次转交会成功。
以下展示了一个函数 g ,其中重载函数以接受左值引用和右值引用。函数 f 将右值引用作为其参数(命名的右值引用)并返回右值引用(未命名的右值引用)。在从 f 调用 g 时,重载解析选择接受左值引用的 g 版本,因为 f 的主体将其参数视为左值。在从 main 调用 g 时,重载解析选择接受右值引用的 g 版本,因为 f 返回右值引用。
// named-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;
// A class that contains a memory resource.
class MemoryBlock
{
// TODO: Add resources for the class here.
};
void g(const MemoryBlock&)
{
cout << "In g(const MemoryBlock&)." << endl;
}
void g(MemoryBlock&&)
{
cout << "In g(MemoryBlock&&)." << endl;
}
MemoryBlock&& f(MemoryBlock&& block)
{
g(block);
return move(block);
}
int main()
{
g(f(MemoryBlock()));
}
输出如下:
In g(const MemoryBlock&).
In g(MemoryBlock&&).
在该示例中,main 函数将一个右值传递给 f。f 的主体将其命名参数视为左值。从 f 到 g 的调用将参数绑定到左值引用(g 的第一个重载版本)。
你可以将左值引用转换为右值引用。
C++ 标准库 std::move 函数可用于将对象转换为该对象的右值引用。您还可以使用 static_cast 关键字将左值转换为右值引用,如以下示例所示:
// cast-reference.cpp
// Compile with: /EHsc
#include <iostream>
using namespace std;
// A class that contains a memory resource.
class MemoryBlock
{
// TODO: Add resources for the class here.
};
void g(const MemoryBlock&)
{
cout << "In g(const MemoryBlock&)." << endl;
}
void g(MemoryBlock&&)
{
cout << "In g(MemoryBlock&&)." << endl;
}
int main()
{
MemoryBlock block;
g(block);
g(static_cast<MemoryBlock&&>(block));
}
此示例产生以下输出:
In g(const MemoryBlock&).
In g(MemoryBlock&&).
函数模板推断其模板参数类型,然后使用引用折叠规则。
函数模板将其参数传递(或转发)给另一个函数是一种常见模式。了解模板类型推导如何适用于采用右值引用的函数模板非常重要。
如果函数参数是右值,编译器会推断该参数为右值引用。例如,假设您将一个对类型 X 的对象的右值引用传递给以类型 T&& 作为参数的函数模板。模板参数推导会推断 T 为 X,因此该参数的类型为 X&&。如果函数参数是左值或 const 左值,编译器会推断其类型为该类型的左值引用或 const 左值引用。
以下示例声明一个结构模板,然后将其特化为各种引用类型。print_type_and_value 函数以右值引用作为其参数,并将其转交给 S::print 方法的适当专用版本。main 函数演示了调用 S::print 方法的各种方式。
// template-type-deduction.cpp
// Compile with: /EHsc
#include <iostream>
#include <string>
using namespace std;
template<typename T> struct S;
// The following structures specialize S by
// lvalue reference (T&), const lvalue reference (const T&),
// rvalue reference (T&&), and const rvalue reference (const T&&).
// Each structure provides a print method that prints the type of
// the structure and its parameter.
template<typename T> struct S<T&> {
static void print(T& t)
{
cout << "print<T&>: " << t << endl;
}
};
template<typename T> struct S<const T&> {
static void print(const T& t)
{
cout << "print<const T&>: " << t << endl;
}
};
template<typename T> struct S<T&&> {
static void print(T&& t)
{
cout << "print<T&&>: " << t << endl;
}
};
template<typename T> struct S<const T&&> {
static void print(const T&& t)
{
cout << "print<const T&&>: " << t << endl;
}
};
// This function forwards its parameter to a specialized
// version of the S type.
template <typename T> void print_type_and_value(T&& t)
{
S<T&&>::print(std::forward<T>(t));
}
// This function returns the constant string "fourth".
const string fourth() { return string("fourth"); }
int main()
{
// The following call resolves to:
// print_type_and_value<string&>(string& && t)
// Which collapses to:
// print_type_and_value<string&>(string& t)
string s1("first");
print_type_and_value(s1);
// The following call resolves to:
// print_type_and_value<const string&>(const string& && t)
// Which collapses to:
// print_type_and_value<const string&>(const string& t)
const string s2("second");
print_type_and_value(s2);
// The following call resolves to:
// print_type_and_value<string&&>(string&& t)
print_type_and_value(string("third"));
// The following call resolves to:
// print_type_and_value<const string&&>(const string&& t)
print_type_and_value(fourth());
}
输出如下:
print<T&>: first
print<const T&>: second
print<T&&>: third
print<const T&&>: fourth
为了解析对 print_type_and_value 函数的每个调用,编译器首先进行模板参数推导。然后,当编译器用推导的模板参数替换参数类型时,它会应用引用折叠规则。例如,将局部变量 s1 传递给 print_type_and_value 函数会导致编译器生成以下函数签名:
print_type_and_value<string&>(string&&& t)
编译器使用引用折叠规则来减少签名:
print_type_and_value<string&>(string& t)
然后,此版本的 print_type_and_value 函数将其参数转发给 S::print 方法的正确专用版本。
下表总结了模板参数类型推导的引用折叠规则:
扩展类型 | 折叠类型 |
T& & | T& |
T& && | T& |
T&& & | T& |
T&& && | T&& |
模板参数推导是实现完备转发的重要元素。
6. 总结
右值引用将左值与右值区分开来。为了提高应用程序的性能,它们可以消除不必要的内存分配和复制操作。它们还使您能够编写一个接受任意参数的函数。该函数可以将它们转交给另一个函数,就像直接调用另一个函数一样。