C++11 中的移动语义和完美转发。






4.93/5 (43投票s)
Visual C++ 11 和 GCC 4.7.0 中的重要特性:移动、右值引用、纯右值、xvalue、完美转发。
引言
在本文中,我将讨论 C++11 中的移动功能,特别强调编写移动构造函数和移动赋值运算符,以及完美转发的问题。其中一些特性在 [1,2] 中得到了很好的阐述,我建议读者观看演示文稿或阅读笔记。与 C++11 标准接近的文档可以在这里找到 [3]。
背景
移动功能旨在优化对象之间的数据传输,尤其是在对象拥有大量在堆上分配的内容时。这样的对象可以看作由两部分组成:一个小的“外壳”(类本身)和指向该外壳的堆分配内容(HAC)。假设我们要将对象 B 的内容复制到 A。在大多数情况下,它们的内容大小是不同的。对象在此处显示
常规操作将是删除 A 的内容
然后,需要分配与 B 的内容相同大小的新空间
接着,需要将 B 的内容复制到 A
通常,如果我们将来需要同时使用对象 A 和 B,应该这样做。但是,如果出于某种原因,对象 B 不需要了(我们将稍后讨论这些原因),则可以交换 A 和 B 的内容
最后一种方法有两个主要优点
(1) 我们不必从堆上分配新内存,这可以减少时间和内存空间;
(2) 我们不必将 B 的内容复制到 A,这节省了复制的处理器时间。
显然,内容越大,收益越大。
右值引用
为了识别适合移动的对象,C++11 中引入了一项新特性,称为 **右值引用**。右值引用通常用于指定对即将被销毁的对象的引用。在 C++11 中,符号 `&&`(两个 ampersand 字符)放在类型名称之后,以标识它是一个右值引用。例如,类 T 的右值引用将被写成:`T&&`。
右值引用的主要用途是指定函数的形参。如果过去,对于(类 T 的)参数,我们使用了两种主要规范
(1) **T&**
(2) **const T&**
第一个用于指定将要被改变的常量和值,第二个用于指定不会被改变的常量和值。
现在,在 C++11 中,有三种选择
(1) **const T&**
(2) **T&**
(3) **T&&**
首先,我们仍然可以使用前两种选择。但是为了使代码更高效,最好利用第三种选择。显然,如果使用第三种选择,前两种选择的覆盖范围就会缩小。
第三种选择指的是右值引用,它对应于那些临时的、即将被销毁的实际参数。让我们看看,哪些实际参数对应于右值引用。这些是函数和表达式,它们返回特定类型的标准值,而不是对它的引用。这解决了 **const T&** 和 **T&** 之间的区别问题
T&:
- **const T&** 指的是变量和常量;
- **T&&** 指的是表达式(包括函数调用),但不是那些返回 **T&** 的表达式。
有一个可能性可以声明某个变量(例如 **x**)的内容是不需要的,可以被视为右值引用,在这种情况下,可以使用 `std::move(x)` 来包装它。我们将在下面讨论。
由于参数有两种可用选项,构造函数和赋值运算符也有两种选项:除了传统的拷贝构造函数和 `copy` 赋值运算符外,现在还有 `move` 构造函数 **T(T&& t)** 和 `move` 赋值运算符 **T operator=(T&& t)**。
如果为特定类定义了移动构造函数和移动赋值运算符,程序就可以利用函数返回值,而不是复制它们,而是交换它们的内容,就像我在图中所示的那样。
在编写移动构造函数和移动赋值运算符时,有一个问题特别重要:被移动的参数的内容应该保持有效,以便后续销毁。这里有两种方法可以使用
(1) 将其内容与目标对象交换(如赋值的情况,我在图片中展示了);
(2) 将其内容设置为一个有效的空值,通过将 `nullptr` 赋给外壳类中的相关指针(通常用于移动构造函数)。
对于移动构造函数,也可以考虑交换,前提是目标对象在交换之前被设置为空值。
带有移动功能的示例类定义
让我们看一个简单的例子:一个定义了 double 值数组的类。大小是固定的,但在赋值时可以改变。你可能见过很多这样的例子。但用于说明很好:它的功能是众所周知的。没有移动语义的 Array 类定义可能如下所示
class Array
{
int m_size;
double *m_array;
public:
Array():m_size(0),m_array(nullptr) {} // empty constructor
Array(int n):m_size(n),m_array(new double[n]) {}
Array(const Array& x):m_size(x.m_size),m_array(new double[m_size]) // copy constructor
{
std::copy(x.m_array, x.m_array+x.m_size, m_array);
}
virtual ~Array() // destructor
{
delete [] m_array;
}
auto Swap(Array& y) -> void
{
int n = m_size;
double* v = m_array;
m_size = y.m_size;
m_array = y.m_array;
y.m_size = n;
y.m_array = v;
}
auto operator=(const Array& x) -> Array& // copy assignment
{
if (x.m_size == m_size)
{
std::copy(x.m_array, x.m_array+x.m_size, m_array);
}
else
{
Array y(x);
Swap(y);
}
return *this;
}
auto operator[](int i) -> double&
{
return m_array[i];
}
auto operator[](int i) const -> double
{
return m_array[i];
}
auto size() const ->int { return m_size;}
friend auto operator+(const Array& x, const Array& y) -> Array // adding two vectors
{
int n = x.m_size;
Array z(n);
for (int i = 0; i < n; ++i)
{
z.m_array[i] = x.m_array[i]+y.m_array[i];
}
return z;
}
};
在赋值中使用 swap 很方便,而且也非常安全。这是一个使用该类的小程序
int main()
{
Array v(3);
v[0] = 2.5;
v[1] = 3.1;
v[2] = 4.2;
const Array v2(v);
Array v3 = v+(v2+v);
std::cout << "v3:";
for (int i = 0; i < 3; ++i)
{
std::cout << " " << v3[i];
};
std::cout << std::endl;
Array v4(3);
v4[0] = 100.1;
v4[1] = 1000.2;
v4[2] = 10000.3;
v4 = v4 + v3;
std::cout << "v4:";
for (int i = 0; i < 3; ++i)
{
std::cout << " " << v4[i];
};
std::cout << std::endl;
return 0;
}
程序将打印
v3: 7.5 9.3 12.6 v4: 107.6 1009.5 10012.9
让我们添加一些移动功能。首先,我们将定义移动构造函数。你可以猜到我们如何开始。参数将是类的右值引用
Array(Array&& x) ...
正如我之前提到的,参数的内容将被移动到对象的内部
Array(Array&& x):m_size(x.m_size),m_array(x.m_array) ...
但重要的是参数返回正确的值:它的内容不能保持不变,它很快就会被销毁。所以,在构造函数的体内部,我们将空值赋给参数的内容。这是移动构造函数的完整定义
Array(Array&& x):m_size(x.m_size),m_array(x.m_array)
{
x.m_size = 0;
x.m_array = nullptr;
}
现在,我们将编写移动赋值运算符。这很容易。参数是右值引用,函数体应该像我们之前讨论的那样,交换当前对象与参数的内容
auto operator=(Array&& x) -> Array&
{
Swap(x);
return *this;
}
就这样。
现在,有一个关于加法运算符的问题。最好也利用右值参数。有两个参数,每个参数有两个选项:应该有四种加法运算符的定义。我们已经写了一个,我们必须写另外三个。让我们从下面的开始
friend auto operator+(Array&& x, const Array& y) -> Array
这非常直接。我们只是修改参数的内容。我们不再需要它了。但我们必须在最后一刻移动它。所以,我们写
friend auto operator+(Array&& x, const Array& y) -> Array
{
int n = x.m_size;
for (int i = 0; i < n; ++i)
{
x.m_array[i] += y.m_array[i];
}
最重要的是最后返回正确的内容。如果我们写 `return x;`,参数的内容将按原样返回。尽管参数的类型是右值,但变量 `x` 是左值:我们还没有做任何事情来将值移出 `x`。如果我们只是返回 `x` 的值,就不会发生移动操作。为了正确地做到这一点,我们必须使用 `return std:move(x);`,这将确保参数的内容与目标对象交换。这是这个运算符的完整定义
friend auto operator+(Array&& x, const Array& y) -> Array
{
int n = x.m_size;
for (int i = 0; i < n; ++i)
{
x.m_array[i] += y.m_array[i];
}
return std::move(x);
}
现在,我们必须编写另外两个运算符。在这里,我们利用加法的交换律。我们交换参数,以便调用前面定义的运算符。
friend auto operator+(Array&& x, Array&& y) -> Array
{
return std::move(y)+x;
}
friend auto operator+(const Array& x, Array&& y) -> Array
{
return std::move(y)+x;
}
但是,正如你所注意到的,我们写了很多代码:四种加法运算符的定义。有可能写得更少吗?为了做到这一点,我们必须看看完美转发。
减少成员函数的数量:完美转发
完美转发专门用于减少程序员需要编写的代码量,尽管它也有其他用途。
让我们考虑最后两个加法运算符的定义。它们有很多共同点,唯一的区别是第一个参数定义不同:`Array&& x` 和 `const Array&& x`。这两个定义可以被替换为使用以下方法
(1) 定义模板 `template
(2) 将相应的参数定义替换为 **T&& x**;
(3) 在使用 `std::move(x)` 的地方,使用 `std::forward(x)`。
在这个特定的代码中,`std::move(x)` 没有被使用,所以我们不必使用 `std::forward(x)`。这是结果
template<class T>
friend auto operator+(T&& x, Array&& y) -> Array
{
return std::move(y)+x;
}
为了将另外两个运算符定义合并成一个,我们必须稍微修改代码,以便两个定义的函数体看起来相似。
但首先,让我们考虑一些关于局部参数的附加信息。如果你看第一个加法运算符,你会发现它有一个名为 **z** 的局部变量。根据 C++11 的规则,当返回该参数时,它将被移动到新的内容(而不是复制!)。你不必使用 `std::move(z)` 来利用移动,实际上只要定义了相关的移动构造函数和移动赋值运算符,它就会被移动。
为了使这两个加法运算符相似,我们可以在两者中创建一个局部变量,将第一个参数的内容转移(复制或移动)到其中,然后将第二个参数的值添加到这个局部变量中。以下是这两个运算符定义的修改版本
friend auto operator+(const Array& x, const Array& y) -> Array
{
int n = x.m_size;
Array z(x);
for (int i = 0; i < n; ++i)
{
z.m_array[i] += y.m_array[i];
}
return z;
}
friend auto operator+(Array&& x, const Array& y) -> Array
{
int n = x.m_size;
Array z(std::move(x));
for (int i = 0; i < n; ++i)
{
z.m_array[i] += y.m_array[i];
}
return z;
}
这正是我们完美转发所需要的。如果我们使用我们之前讨论的三条规则,我们可以重写这两个运算符如下
template<class T>
friend auto operator+(T&& x, const Array& y) -> Array
{
int n = x.m_size;
Array z(std::forward<T>(x));
for (int i = 0; i < n; ++i)
{
z.m_array[i] += y.m_array[i];
}
return z;
}
你可能会认为我们的一些修改使代码变慢了。为了效率起见,最好将前两个运算符定义保持不变。
整合所有内容:让我们运行一些测试
为了了解移动功能的优势,我必须在代码中添加额外的打印输出来查看调用了哪些成员函数。这将有助于衡量效率。
//MOVE SEMANTICS REVISED
#include <iostream>
#include <string>
#include <algorithm>
#include <cmath>
#define MOVE_FUNCTIONALITY
int count_copies = 0;
int count_allocations = 0;
int elem_access = 0;
class Array
{
int m_size;
double *m_array;
public:
Array():m_size(0),m_array(nullptr) {}
Array(int n):m_size(n),m_array(new double[n])
{
count_allocations += n;
}
Array(const Array& x):m_size(x.m_size),m_array(new double[m_size])
{
count_allocations += m_size;
count_copies += m_size;
std::copy(x.m_array, x.m_array+x.m_size, m_array);
}
#ifdef MOVE_FUNCTIONALITY
Array(Array&& x):m_size(x.m_size),m_array(x.m_array)
{
x.m_size = 0; // clearing the contents of x
x.m_array = nullptr;
}
#endif
virtual ~Array()
{
delete [] m_array;
}
auto Swap(Array& y) -> void
{
int n = m_size;
double* v = m_array;
m_size = y.m_size;
m_array = y.m_array;
y.m_size = n;
y.m_array = v;
}
#ifdef MOVE_FUNCTIONALITY
auto operator=(Array&& x) -> Array&
{
Swap(x);
return *this;
}
#endif
auto operator=(const Array& x) -> Array&
{
if (x.m_size == m_size)
{
count_copies += m_size;
std::copy(x.m_array, x.m_array+x.m_size, m_array);
}
else
{
Array y(x);
Swap(y);
}
return *this;
}
auto operator[](int i) -> double&
{
elem_access++;
return m_array[i];
}
auto operator[](int i) const -> double
{
elem_access++;
return m_array[i];
}
auto size() const ->int { return m_size;}
#ifdef MOVE_FUNCTIONALITY
template<class T>
friend auto operator+(T&& x, const Array& y) -> Array
{
int n = x.m_size;
Array z(std::forward<T>(x));
for (int i = 0; i < n; ++i)
{
elem_access+=2;
z.m_array[i] += y.m_array[i];
}
return z;
}
template<class T>
friend auto operator+(T&& x, Array&& y) -> Array
{
return std::move(y)+x;
}
#else
friend auto operator+(const Array& x, const Array& y) -> Array
{
int n = x.m_size;
Array z(n);
for (int i = 0; i < n; ++i)
{
elem_access += 3;
z.m_array[i] = x.m_array[i] + y.m_array[i];
}
return z;
}
#endif
void print(const std::string& title)
{
std::cout << title;
for (int i = 0; i < m_size; ++i)
{
elem_access++;
std::cout << " " << m_array[i];
};
std::cout << std::endl;
}
};
int main()
{
const int m = 100;
const int n = 50;
{
Array v(m);
for (int i = 0; i < m; ++i) v[i] = sin((double)i);
const Array v2(v);
Array v3 = v+(v2+v);
v3.print("v3:");
Array v4(m);
for (int i = 0; i < m; ++i) v4[i] = cos((double)i);
v4 = v4 + v3;
v4.print("v4:");
Array v5(n);
for (int i = 0; i < n; ++i) v5[i] = cos((double)i);
Array v6(n);
for (int i = 0; i < n; ++i) v6[i] = tan((double)i);
Array v7(n);
for (int i = 0; i < n; ++i) v7[i] = exp(0.001*(double)i);
Array v8(n);
for (int i = 0; i < n; ++i) v8[i] = 1/(1+0.001*(double)i);
v4 = (v5+v6)+(v7+v8);
v4.print("v4 new:");
}
std::cout << "total allocations (elements):" << count_allocations << std::endl;
int total_elem_access = count_copies*2 + elem_access;
std::cout << "total elem access (elements):" << total_elem_access << std::endl;
return 0;
}
在我对之前版本的代码进行了一些测试后,我意识到有必要计算总的元素访问实例数,而不是只计算复制操作(一次复制算作两次元素访问实例:1 源 + 1 目标)。以下是结果表格(GCC 4.7.0 和 Visual C++ 2011 的发布模式):
计数器 | 复制 | 移动 |
元素分配 | 1000 | 800 |
元素访问 | 2500 | 2350 |
复制省略
在某些情况下,C++11 标准允许省略复制或移动构造,即使构造函数和/或析构函数有副作用。这个特性称为 **复制省略**。复制省略允许在以下情况下进行
- 在函数中的 return 语句中,当返回值是局部(非易失性)变量,并且其类型与函数返回类型相同(在此,类型比较时,忽略 `const` 和其他限定符);我之前已经提到了这种情况;
- 在 throw 表达式中,当操作数是一个局部、非易失性变量,其作用域不超出最近的包含 try 块的结束。
- 当一个临时对象(未绑定到引用)将被复制或移动到一个具有相同类型的类对象(忽略类型限定符,与前一种情况相同);
- 在异常声明(在 try-catch 块中),当 catch 中的参数与 throw 语句中的对象类型相同时(忽略类型限定符,与前一种情况相同)。
在所有这些情况下,对象都可以直接构造或移动到目标。在所有这些情况下,不仅可以省略复制,甚至可以省略移动。在所有这些情况下,右值引用首先被考虑(移动),然后才是左值引用(复制),之后才发生复制省略。
这是一个使用我们之前讨论过的数组类的示例程序
const Array f()
{
Array z(2);
z[0] = 2.1;
z[1] = 33.2;
return z;
}
Array f1()
{
Array z(2);
z[0] = 2.1;
z[1] = 33.2;
return z;
}
void g(Array&& a)
{
a.print("g(Array&&)");
}
void g(const Array& a)
{
a.print("g(const Array&)");
}
void pf(Array a)
{
a.print("pf(Array)");
}
int main()
{
{
Array p(f());
p.print("p");
g(f());
g(f1());
pf(f());
}
std::cout << "total allocations (elements):" << count_allocations << std::endl;
int total_elem_access = count_copies*2 + elem_access;
std::cout << "total elem access (elements):" << total_elem_access << std::endl;
return 0;
}
在 Visual C++ 11 的调试模式下,当定义了移动构造函数时,程序会打印以下内容
p 2.1 33.2
g(const Array&) 2.1 33.2
g(Array&&) 2.1 33.2
pf(Array) 2.1 33.2
total allocations (elements):8
total elem access (elements):16
首先,如果您查看 `g` 函数调用,根据使用 `f()` 还是 `f1()` 作为参数,会选择正确的重载函数。但这并不能阻止编译器在所有这些调用中使用移动操作(或完全省略它)。
您可以查看带有额外打印的类仪表化版本,以跟踪执行的是哪个构造函数。在 Visual C++ 11 的调试模式下,没有移动功能,结果是
总分配(元素):16
总元素访问(元素):32
在发布模式下,这些数字始终是 8 和 16,并且编译器省略了移动或复制,两者都没有执行。
GCC 4.7.0 即使在调试模式下,也始终省略移动和复制。
从编程的角度来看,这意味着什么。首先,如果您不编写带有右值参数的重载函数版本,您仍然可以利用移动操作:省略就像优化的移动。一般方法是
- 定义复制和移动构造函数;
- 定义可以利用移动的函数或运算符(如 Array 类的 operator+)。
在所有其他情况下,当优化不明显时,请以传统的方式编写函数,使用左值引用(T& 或 const T&)作为参数。
值类别
C++11 中的每个表达式都属于以下类别之一:`lvalue`、`xvalue` 和 `prvalue`。它们都可以是 const 和 non-const,尽管有些值不具有 const 性(如字面量和枚举值)。让我们看看所有这些类别。
- lvalue 指的是一个函数或一个非临时对象,它可以被引用,通常要么有名称,要么被指向(例如,变量、函数形参、用户定义的常量(使用 const 声明)和被指针指向的对象;由函数返回的 T& 类型值(其中 T 本身不包含引用)也是一个 lvalue(例如,函数 `Array& f1()` 将返回一个 l-value);
- prvalue 指的是一个字面量(例如 27、2.5、‘a’、true)、一个枚举值,
一个函数或运算符的值,它不是引用(例如,由函数返回的类对象;函数 `Array f2()` 返回一个 prvalue);
- xvalue 指的是某个类型 T&& 的值(其中 T 本身不包含引用),它是用户定义函数、`std::move` 或转换为 T&& 的转换运算符的结果(例如,`static_cast
(x)` 和函数 `Array&& f3()` 返回一个 xvalue)。
glvalue 是 lvalue 或 xvalue。rvalue 是 xvalue 或 prvalue。
任何函数形参、变量或用户定义的常量(非枚举值)都是 lvalue。
至于 prvalue 常量,它们只能是类对象,由函数或运算符返回。以下函数返回一个 const prvalue
const Array fc()
{
Array p(2);
p[0] = 3.2;
p[1] = 2.2;
return p;
}
但这个函数的问题是,你不能将它的结果赋值给定义为 `Array&&` 的变量或参数。最好在这里不使用 `const`。
prvalue,包括字面量和枚举值,都被认为是 non-const,可以赋值给右值引用类型的变量和参数。尽管实际上我们很少使用简单类型的右值引用。
至于 `xvalue`,`std::move(x)` 或 `static_cast
下表解释了根据这些值的类别将值赋给变量、常量(初始化)和参数的规则(缩写表示:C - 复制,M - 移动,R - 传递引用,E - 可能的复制省略,t - 允许类型转换,X - 非法):
变量、形参或常量 | 值 | ||||
声明 | 类别 | non-const prvalue | const lvalue | non-const lvalue | xvalue |
**T** 或 **const T** | lvalue 或 const lvalue | E/M/t | C/t | C/t | M/t |
T& | lvalue | X | X | [R] | X |
T&& | lvalue | [R]/ t | X | X | [R]/t |
const T& | const lvalue | R/t | [R]/t | R/t | R/t |
在方括号中,显示了重载函数的首选选择,以防定义了三个重载 `f(T&)`、`f(T&&)` 和 `f(const T&)`。不幸的是,如果同时定义了 `f(T)` 和 `f(T&)`,则使用左值实际参数调用这些函数可能会导致歧义,这是不允许的。
再次提醒您,所有这些变量、参数或常量都属于 lvalue 类别,因此不能作为 rvalue 传递;如果您想这样做,应该使用 `std::move`(或转换)。
当发生类型转换时(因为值的类型与变量的类型不同),通常会创建一个新的临时对象,该对象被视为 prvalue。我们主要对 T 是类对象的类型的值感兴趣。表格显示,当 prvalue 和 xvalue 传递给参数或赋值给变量时,操作非常高效。当 lvalues 传递给声明为 **T** 或 **const T** 类型的参数时,会发生复制。
Microsoft 编译器在将 prvalue 传递给 T& 参数时更灵活:编译器允许这样做,这是对 C++ 标准的扩展。标准不允许这样做。
在偏好方面,如果 **T** 是一个类,避免为参数使用 **T** 或 **const T**,除非您确实需要实际参数的副本。使用 **T, T&& 或 const T&**。为了实现 rvalue 传递的效率,在类中定义移动构造函数和移动赋值运算符是很好的。通常,如前所述,`T&&` 可用于使某些函数更高效(例如,为 operator+ 添加额外代码);否则,使用 `const T&` 按值传递参数效率很高。
在您编写的函数(而非移动构造函数或移动赋值运算符)方面,对于 prvalue,无论您编写 **T&&** 还是 **const T&**,通常没有区别,因为复制省略会使参数传递非常高效。但是 **const T&** 允许您通过引用传递 lvalues,这非常高效。
在函数返回值方面:使用 **T, T&** 或 **const T&**。
如果您再次查看表格的第一行,这是省略、移动和复制选择很重要的地方。赋值的一般规则是,如果源是临时对象(prvalue)并且目标是变量,则将发生移动赋值。对于初始化或参数传递,如果源是临时对象,则发生复制省略。但可能会出现源是“隐藏”lvalue 的情况。考虑以下函数
Array g(Array& y) { return std::move(y); }
以下是一段使用该函数的代码片段
Array x(1); v[0] = 2.5; Array z(g(x));
在这种情况下,**x** 仍然是一个有效的 lvalue,而不是一个临时对象,它被 `y` 形参引用。结果是,当初始化 **z** 时,将发生移动构造。
以下是实际参数调用重载的具有右值引用函数时的条件
- 调用不返回左值引用的函数或运算符(**T&**);
- 调用构造函数,该构造函数创建一个临时对象;
- 必须对 const 左值引用参数应用转换,这会创建一个新的临时对象;使用了 `std::move(x)` 或 `static_cast
(x)`; - 使用了字面量(例如:2.0、true、‘A’),在这种情况下会创建一个临时对象来包含它。
这是一个说明这些规则的程序
#include <iostream> #include <string> class A { int i; public: A():i(3){} A(int j):i(j) {} int getInt() const { return i;} void setInt(int j) { i = j;} }; auto f(A&& a) -> void { a.setInt(a.getInt()+1); std::cout << "f(A&&):" << a.getInt() << std::endl; } auto f(const A& a) -> void { std::cout << "f(const A&):" << a.getInt() << std::endl; } auto g() -> A { return A(27); } auto f(const std::string& s) -> void { std::cout << "f(const string&):" << s << std::endl; } auto f(std::string&& s) -> void { std::cout << "f(string&&):" << s << std::endl; } auto f(const int& i) -> void { std::cout << "f(const int&):" << i << std::endl; } auto f(int&& i) -> void { i++; std::cout << "f(int&&):" << i << std::endl; } int main() { f(A()); // a temporary object f(g()); // a temporary value returned A x(21); f(std::move(x)); // an explicit std::move is used std::cout << "x: " << x.getInt() << std::endl; f("abc"); // creates a temporary string first f(7); int z = 100; f(std::move(z)); // an explicit std::move is used std::cout << "z: " << z << std::endl; return 0; }
此程序将打印
f(A&&):4 f(A&&):28 f(A&&):22 x: 22 f(string&&):abc f(int&&):8 f(int&&):101 z: 101
观察副作用。右值引用可用于更改函数参数的值。在某种意义上,它们类似于普通引用。
访问临时对象的成员。成员函数的引用限定符
让我们看下面的代码
struct Container
{
Array m_v;
public:
Container():m_v(2)
{
m_v[0] = 177.1;
m_v[1] = 12.7;
}
Container(const Array& x):m_v(x) {}
Array moveArray() { return std::move(m_v);}
Array getArray() const { return m_v;}
};
int main()
{
{
Array zz(Container().moveArray()); // uses move
Array k(Container().getArray()); // uses copy
zz.print("zz");
}
return 0;
}
这里 `Container` 类用于一个临时对象。我们为 Array 对象创建了两个访问函数:`getArray` 和 `moveArray`。第一个更有效:它使用移动操作。但我们明确地给它们起了不同的名字。
如果能够定义重载的访问函数,它们会根据对象是否是临时对象而执行不同的操作,那就太好了。在 C++11 标准中,这是可能的,并且可以使用引用限定符(**&** 或 **&&**)来实现。
Array getArray() && { return std::move(m_v);} Array getArray() const& { return m_v;}
规则是,如果您为其中一个重载函数使用引用限定符,则必须为所有其他函数使用引用限定符。不幸的是,Visual C++ 11 或 GCC 4.7.0 中不提供此功能。
完美转发的其他用途
除了之前讨论的情况外,还有以下情况,使用完美转发很方便
- 定义一个函数模板,该模板将创建一个类对象,并可能对其进行额外的操作;
- 创建一个函数调用的包装器。
第一种情况常用于新的 C++11 STL 中的 `emplace(...)` 方法,它们同时创建并将对象存储在容器中。
例如,对于类 vector<A> 的 `v`,而不是使用 `v.push_back(A(p1, p2, ..., pn))`,我们可以使用 `v.emplace_back(p1, p2, ..., pn)`。为了为主构造函数提供任意数量参数的功能,需要可变参数模板。它们在 Visual C++ 11 中不可用,但在 GNU C++ 4.7.0 中可用。
另一个例子,这是 `make_unique` 的定义,它是一个返回给定类唯一指针的函数模板
template<typename T, typename... U> auto make_unique(U&&... p) -> std::unique_ptr<T> { return std::unique_ptr<T>(new T(std::forward<U>(p)...)); }
在 Visual C++ 11 中,您可以为具有单参数构造函数的类定义类似的模板。
创建包装器可以用于,例如,当有一个函数 **t**,带有一个参数,但有多个重载定义时。您可以创建一个包装器,如下所示
template<class U>
void f(U&& x)
{
std::cout << "Calling function t... " << x << std::endl;
t(std::forward<U>(x));
}
有一个类似的包装器情况,其中函数被提供为一个参数。这种情况不是严格的完美转发:参数类型不是从值 `x` 派生的,而是从函数派生的。在这种情况下,您可以使用 `static_cast
template<class R, class T, class U>
auto call(auto (*g)(T) -> R, U&& x) -> R
{
std::cout << "call(g, x)" << x << std::endl;
return g(static_cast<T>(x)); //return g(std::forward<T>(x));
我们仍然必须使用 U&&:否则,一些引用参数将无法正常工作。
这是一个说明所有这些特性的程序
#include <iostream>
#include <memory>
#include <utility>
struct A {
A(const int& n) { std::cout << "A(const int&), n=" << n << "\n"; }
A(int&& n) { std::cout << "A(int&&), n=" << n << "\n"; }
A(int& n) { n++; std::cout << "A(int&), n=" << n << "\n"; }
};
template<class T, class U>
auto make_unique(U&& u) -> std::unique_ptr<T>
{
return std::unique_ptr<T>(new T(std::forward<U>(u)));
}
auto t(int& x) -> void
{
x++;
std::cout << "t(int& x): " << x << std::endl;
}
auto t(int&& x) -> void
{
std::cout << "t(int&& x): " << x << std::endl;
}
auto t(const int& x) -> void
{
std::cout << "t(const int& x): " << x << std::endl;
}
template<class U>
auto f(U&& x) -> void
{
std::cout << "Calling function t... " << x << std::endl;
t(std::forward<U>(x));
}
auto ten_times(int i) -> int
{
return 10*i;
}
auto g1(const int& n) -> void { std::cout << "g1(const int&), n=" << n << "\n"; }
auto g2(int&& n) -> void { std::cout << "g2(int&&), n=" << n << "\n"; }
auto g3(int& n) -> int& { n++; std::cout << "g3(int&), n=" << n << "\n"; return n; }
auto g4(int n) -> int { std::cout << "g4(int), n=" << n << "\n"; return 4*n; }
template<class R, class T, class U>
auto call(auto (*g)(T) -> R, U&& x) -> R
{
std::cout << "call(g, x)" << x << std::endl;
return g(static_cast<T>(x)); //return g(std::forward<T>(x));
}
int main()
{
const int j = 5;
std::unique_ptr<A> pj = make_unique<A>(j);
std::unique_ptr<A> p1 = make_unique<A>(2);
int i = 10;
std::unique_ptr<A> p2 = make_unique<A>(i);
std::cout << "i: " << i << std::endl;
f(i);
std::cout << "after call f(i). i: " << i << std::endl;
f(j);
f(ten_times(8));
std::cout << "i: " << i << std::endl;
A a(i);
call(g1,j);
call(g2,2);
int& k = call(g3, i);
k *= 100;
std::cout << "i: " << i << " k: " << k << std::endl;
int k2 = call(g4,7);
std::cout << "k2: " << k2 << std::endl;
return 0;
}
程序将打印
A(const int&), n=5 A(int&&), n=2 A(int&), n=11 i: 11 Calling function t... 11 t(int& x): 12 after call f(i). i: 12 Calling function t... 5 t(const int& x): 5 Calling function t... 80 t(int&& x): 80 i: 12 A(int&), n=13 call(g, x)5 g1(const int&), n=5 call(g, x)2 g2(int&&), n=2 call(g, x)13 g3(int&), n=14 i: 1400 k: 1400 call(g, x)7 g4(int), n=7 k2: 28
U&& 很重要:在此程序中,如果您将 **U&& x** 更改为 **U x**,您将无法为 i 或 k 获得正确的值 1400。
只能移动,永远不能复制的类
有一些类只能被移动(在 [4] 中提到),例如:`fstream` 和 `unique_ptr`。这是一个带有 `ifstream` 的程序,演示了这个特性
#include <iostream>
#include <string>
#include <fstream>
std::ifstream OpenMyFile(const std::string& filename)
{
std::ifstream file(filename);
if (!file.good())
{
file.close();
return file;
}
std::string s;
std::getline(file, s);
if (s.substr(0,7) != "My File")
{
file.close();
file.setstate(std::ios_base::failbit);
return file;
}
return file;
}
int main(int count, char *args[])
{
if (count < 2)
{
std::cout << "Incorrect number of parameters" << std::endl;
return -1;
}
std::ifstream myFile = OpenMyFile(args[1]);
if (myFile.good())
{
while (!myFile.eof())
{
std::string s;
std::getline(myFile,s);
std::cout << s << std::endl;
}
}
else
{
std::cout << "*** FILE ERROR ***" << std::endl;
}
return 0;
}
如果我们使用该程序的参数,文件 `myfile.txt` 具有以下内容
My File Some types are not amenable to copy semantics but can still be made movable. For example: (1) fstream (2) unique_ptr (non-shared, non-copyable ownership) (3) A type representing a thread of execution
程序将打印文件的行(第一行除外)。但如果第一行不是“My File”,程序将报告错误。
此程序的主要特性是 `OpenMyFile` 函数,它返回局部变量 `file` 的值。您可能还记得,在这种情况下,返回值可以被移动。此值将被移动到主程序中的变量 `myFile`。
不幸的是,最后一个程序仅在 Visual C++ 11 中运行。在 GCC 4.7.0 中,fstream 没有实现移动语义。
参考文献
- Scott Meyers. "Move Semantics, Rvalue References, and Perfect Forwarding". Notes in PDF.
http://www.aristeia.com/TalkNotes/ACCU2011_MoveSemantics.pdf - Scott Meyers. "Move Semantics, Rvalue References, and Perfect Forwarding". Presentation.
http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references - C++ Working Draft,
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf - Howard E. Hinnant, Bjarne Stroustrup, and Bronek Kozicki. "A Brief Introduction to Rvalue References".
http://www.artima.com/cppsource/rvalue.html