65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (43投票s)

2012年6月4日

CPOL

20分钟阅读

viewsIcon

107535

downloadIcon

947

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(x)` 是很好的例子。最好不要定义返回 xvalue 的函数,普通的类(值 **T**)就足够了。

下表解释了根据这些值的类别将值赋给变量、常量(初始化)和参数的规则(缩写表示: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** 时,将发生移动构造。

以下是实际参数调用重载的具有右值引用函数时的条件

  1. 调用不返回左值引用的函数或运算符(**T&**);
  2. 调用构造函数,该构造函数创建一个临时对象;
  3. 必须对 const 左值引用参数应用转换,这会创建一个新的临时对象;使用了 `std::move(x)` 或 `static_cast(x)`;
  4. 使用了字面量(例如: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(x)` 或 `std:forward(x)`:

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 没有实现移动语义。

参考文献

  1. Scott Meyers. "Move Semantics, Rvalue References, and Perfect Forwarding". Notes in PDF.
    http://www.aristeia.com/TalkNotes/ACCU2011_MoveSemantics.pdf
  2. Scott Meyers. "Move Semantics, Rvalue References, and Perfect Forwarding". Presentation.
    http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references
  3. C++ Working Draft,
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf
  4. Howard E. Hinnant, Bjarne Stroustrup, and Bronek Kozicki. "A Brief Introduction to Rvalue References".
    http://www.artima.com/cppsource/rvalue.html
© . All rights reserved.