C++11:std::move和std::forward

std::move强制转化为右值

  既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

#include<utility>
int a;
int &&r1 = a;           // 编译失败
int &&r2 = std::move(a);    // 编译通过

  从实际上讲, std::move 相当于一个类型转换 static_cast<T&&> (lvalue)
常见的使用方法如下:

#include <iostream>
#include <utility>
#include <type_traits>
using namespace std;

class Base {
public:
        Base(int size) :sz(size > 0 ? size : 1) { c = new int[sz]; }
        ~Base() { delete[] c; }
        Base(Base&& tmp) :sz(tmp.sz), c(tmp.c) { 
		tmp.c = nullptr; 
		cout << "Base 移动构造被调用" << endl;
	}
public:
        int* c;
        int sz;
};

class Son {
public:
        Son() :i(new int(3)), base(1024) {}
        ~Son() { delete i; }
        Son(Son&& tmp) :i(tmp.i), base(move(tmp.base)) {  //强制转化为右值,以调用移动构造函数
		tmp.i = nullptr; 
		cout << "Son 移动构造函数被调用" << endl;
	}
public:
        int* i;
        Base base;
};

Son GetTemp() {
        Son tmp = Son();  //创建匿名对象为右值,调用两次移动构造
        cout << hex << "Son Men from " << __func__ << " @" << tmp.base.c << endl;
        return tmp;  //返回临时对象,调用两次移动构造
}

int main() {
        Son tmp(GetTemp());  //同理,两次移动构造
        cout << hex << "Son Men from " << __func__ << " @" << tmp.base.c << endl;
        return 0;
}

上述代码输出结果为

Base 移动构造被调用
Son 移动构造函数被调用
Son Men from GetTemp @0x55fbaafe1e90
Base 移动构造被调用
Son 移动构造函数被调用
Base 移动构造被调用
Son 移动构造函数被调用
Son Men from main @0x55fbaafe1e90

有了移动语义后,还有一个典型的应用就是可以实现高性能的 swap 函数

template<class T>
void swap(T& a, T& b) {
	T tmp(move(a));
	a = move(b);
	b = move(tmp);
}

上述交换的过程都通过指针交换完成,不会存在资源的释放与申请,极大的增加了程序的执行效率。

完美转发

  所谓完美转发(perfect forwarding)是指在函数模板中,完全按照模板的参数的类型,将参数传递给函数模板中调用另外一个函数。

  完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数

  “原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:左值/右值和 const/non-const。完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变,同时,而不产生额外的开销,就好像转发者不存在一样。在泛型函数中,这样的需求非常普遍。

template <class T>
void process_value(T& val) {
	cout << "T &" << endl;
}
template <typename T>
void process_value(const T& val) {
	cout << "const T &" << endl;
}

//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T>
void forward_value(const T& val) {
	process_value(val);
}

template <typename T>
void forward_value(T& val) {
	process_value(val);
}



void test2() {
	int a = 0;
	const int& b = 1;//常量左值引用能接收任何引用

	 //函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&
	forward_value(a); // T&
	forward_value(b);// const T &
	forward_value(2);// const T&
}

  对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。

  那 C++ 11 是如何解决完美转发的问题的呢?实际上,C++ 11 是通过引入一条所谓“引用折叠”(reference collapsing)的新语言规则,并结合新的模板推导规则来完成完美转发。

typedef const int T;
typedef T & TR;
TR &v = 1; //在C++11中,一旦出现了这样的表达式,就会发生引用折叠,即将复杂的未知表达式折叠为已知的简单表达式

C++11中的引用折叠规则:
|TR的类型定义|声明v的类型|v的实际类型|
|-------|-------|-------|
|T &|TR|T &|
|T &|TR &|T &|
|T &|TR &&|T &|
|T &&|TR|T &&|
|T &&|TR &|T &|
|T &&|TR &&|T &&|

  一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。
  所以当引入上述规则后,当调用转发函数时传入了一个 T 类型为左值引用时,转发函数将会被实例化成如下

void IamForwording(T& && t) {
	IrunCodeActually(T& t)
}

  在C++ 11 中用于完美转发的函数不在时 std::move 进行左右值转换,而是 std::forward,所以转发函数可以写成如下形式:

template <class T>
void process_value(T& val) {
	cout << "T &" << endl;
}

template <class T>
void process_value(T&& val) {
	cout << "T &&" << endl;
}

template <typename T>
void process_value(const T& val) {
	cout << "const T &" << endl;
}

template <typename T>
void process_value(const T&& val) {
	cout << "const T &&" << endl;
}

//函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value
template <typename T>
void forward_value(const T& val) {
	process_value(val);
}

template <typename T>
void forward_value(T&& val) {//参数为右值引用
	process_value(std::forward<T>(val));//C++11中,std::forward可以保存参数的左值或右值特性
}



void test2() {
	int a = 0;
	const int& b = 1;//常量左值引用能接收任何引用

	forward_value(a);// T &
	forward_value(b);// const T &
	forward_value(2);// T &&
	forward_value(std::move(b));// const T &&
}

  通过上述输出可以发现,4种类型的值都被正确的转发到了合理的地方。完美转发的一个作用就是包装函数,可以用很少的代码记录单参数函数的参数传递状况。