C++的std::move与std::forward原理总结
阅读大型的C++开源项目代码,基本逃不过std::move
和std::forward
,例如 webRTC 。所以搞懂其原理,很有必要。
0、左值与右值的理解
左值和右值的概念
C++中左值(lvalue)和右值(rvalue)是比较基础的概念,虽然平常几乎用不到,但C++11之后变得十分重要,它是理解 move/forward 等新语义的基础。
左值与右值这两个概念是从 C 中传承而来的,左值指既能够出现在等号左边,也能出现在等号右边的变量;右值则是只能出现在等号右边的变量。
左值和右值主要的区别之一是左值可以被修改,而右值不能。
能用取址符号 & 取出地址的皆为左值,剩下的都是右值。并且,匿名变量一律属于右值。
int a; // a 为左值
a = 3; // 3 为右值
int b = 1; // b 是左值,1 是右值
int GetZero {
int zero = 0;
return zero;
}
// c 是左值,GetZero() 函数的临时返回值是右值,因为返回值存在于寄存器中
int c = GetZero();
//d 是左值,string("no name") 是匿名变量,是右值
string d = string("no name");
- 左值是可寻址的变量,有持久性;
- 右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。
左值引用和右值引用
- 左值引用:引用一个对象;
- 右值引用:就是必须绑定到右值的引用,C++11中右值引用可以实现“移动语义”,通过 && 获得右值引用。
int x = 6; // x是左值,6是右值
int &y = x; // 左值引用,y引用x
int &z1 = x * 6; // 错误,x*6是一个右值
const int &z2 = x * 6; // 正确,可以将一个const引用绑定到一个右值
int &&z3 = x * 6; // 正确,右值引用
int &&z4 = x; // 错误,x是一个左值
右值引用和相关的移动语义是C++11标准中引入的最强大的特性之一,通过std::move()可以避免无谓的复制,提高程序性能。
1. std::move
别看它的名字叫 move,其实std::move
并不能移动任何东西,它唯一的功能是将一个左值/右值强制转化为右值引用,继而可以通过右值引用使用该值,所以称为移动语义
。
std::move
的作用:将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能。 它是怎么个转移法,将在文章的最后面解释。
看到std::move的代码,意味着给std::move的参数,在调用之后,就不再使用了。
1.1 函数原型
/**
* @brief Convert a value to an rvalue.
* @param __t A thing of arbitrary type.
* @return The parameter cast to an rvalue-reference to allow moving it.
*/
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
用到的remove_reference
定义
/// remove_reference
template<typename _Tp>
struct remove_reference {
typedef _Tp type;
};
template<typename _Tp>
struct remove_reference<_Tp&> {
typedef _Tp type;
};
template<typename _Tp>
struct remove_reference<_Tp&&> {
typedef _Tp type;
};
1.2 参数讨论
先看参数 T&& t
,其参数看起来是个右值引用,其是不然!!!
因为 T 是个模板,当右值引用和模板结合
的时候,就复杂了。T&&
并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。
再弄个清爽的代码解释一下:
template<typename T>
void func( T&& param){
}
func(5); // 5是右值,param是右值引用
int a = 10;
func(a); // x是左值,param是左值引用
这里的&&
是一个未定义的引用类型,称为通用引用 Universal References
(https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers)
它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用。
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个Universal References。
1.3 通用引用
这里还可以再深入一下通用引用,解释为什么它一会可以左值引用,一会可以右值引用。
既然 T 是个模板,那 T 就可以是 string ,也可以是 string& ,或者 string&& 。
那参数就变成string&& && param
了,这么多 & 怎么办?好吓人!!!没事,稳住,C++ 11立了规矩,太多&就要折叠一下(也就是传说中的引用折叠)。具体而言
X& &、X&& &、X& &&都折叠成X&
X&& &&折叠成X&&
所以,想知道 param 最终是什么引用,就看 T 被推导成什么类型了。
可以用下面的一个测试程序,来验证。
#include <iostream>
#include <type_traits>
#include <string>
using namespace std;
template<typename T>
void func(T &¶m) {
if (std::is_same<string, T>::value) {
std::cout << "string" << std::endl;
} else if (std::is_same<string &, T>::value) {
std::cout << "string&" << std::endl;
} else if (std::is_same<string &&, T>::value) {
std::cout << "string&&" << std::endl;
} else if (std::is_same<int, T>::value) {
std::cout << "int" << std::endl;
} else if (std::is_same<int &, T>::value) {
std::cout << "int&" << std::endl;
} else if (std::is_same<int &&, T>::value) {
std::cout << "int&&" << std::endl;
} else {
std::cout << "unkown" << std::endl;
}
}
int getInt() { return 10; }
int main() {
int x = 1;
// 传递参数是右值 T 推导成了int, 所以是 int&& param, 右值引用
func(1);
// 传递参数是左值 T 推导成了int&, 所以是int&&& param, 折叠成 int&,左值引用
func(x);
// 参数getInt是右值 T 推导成了int, 所以是int&& param, 右值引用
func(getInt());
return 0;
}
1.4 返回值
以 T 为 string 为例子,简化一下函数定义:
// T 的类型为 string
// remove_reference<T>::type 为 string
// 整个std::move 被实例如下
string&& move(string&& t) { // 可以接受右值
return static_cast<string&&>(t); // 返回一个右值引用
}
显而易见,用static_cast
,返回的一定是个右值引用。
综上,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);
即,输入可以是左值,右值,输出,是一个右值引用。
1.5 std::move的常用例子
1.5.1 用于vector添加值
以下是一个经典的用例:
// 摘自https://zh.cppreference.com/w/cpp/utility/move
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main() {
std::vector<std::string> v;
std::string str = "Hello";
// 调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0] << "\", \"" << v[1] << "\"\n";
}
// 输出
// After copy, str is "Hello"
// After move, str is ""
// The contents of the vector are "Hello", "Hello"
1.5.2 用于unique_ptr传递
#include <memory>
#include <iostream>
/* 类定义开始 */
class TestC {
public:
TestC(int tmpa, int tmpb) : a(tmpa), b(tmpb) { std::cout << "construct TestC " << std::endl; }
~TestC() { std::cout << "destruct TestC " << std::endl; }
void print() { std::cout << "print a " << a << " b " << b << std::endl; }
private:
int a = 10;
int b = 5;
};
/* 类定义结束 */
void TestFunc(std::unique_ptr<TestC> ptrC) {
printf("TestFunc called \n");
ptrC->print();
}
int main(int argc, char *argv[]) {
std::unique_ptr<TestC> a(new TestC(2, 3));
// 初始化也可以写成这样
// std::unique_ptr<TestC> a = std::make_unique<TestC>(2, 3);
TestFunc(std::move(a));
// 执行下面这一句会崩溃(Process finished with exit code -1073741819 (0xC0000005)),因为a已经没有控制权
a->print();
return 0;
}
// 执行后输出
// construct TestC
// TestFunc called
// print a 2 b 3
// destruct TestC
从日志可见,只有一次构造。
这种类型的代码,在大型开源项目,如 webRTC ,随处可见。下次看到了不用纠结,不用关心细节了。只要直到最后拿到 unique_ptr 的变量(左值)有控制权就行了。
1.6 再说转移对象控制权
从上面1.5.2
的例子,看到std::move(a)
之后,执行a->print();
后会崩溃,这是为什么呢?
其实不全部是std::move
的功劳,还需要使用方,即unique_ptr
配合才行。
请看这篇文章:https://blog.csdn.net/newchenxf/article/details/116274506
当调用TestFunc(std::move(a));
时TestFunc
的参数 a 要初始化,调用的是operator=
,关键代码截取如下:
class unique_ptr {
private:
T* ptr_resource = nullptr;
// ...
// "="号赋值操作,此处的 move 即为 a
unique_ptr& operator=(unique_ptr&& move) noexcept {
move.swap(*this); // 置换后 a 就变成空了,丢失了原来的控制权
return *this;
}
// 交换函数
void swap(unique_ptr<T>& resource_ptr) noexcept {
std::swap(ptr_resource, resource_ptr.ptr_resource); // 这里把空换给了 a
}
从operator=
函数看,执行完赋值后,智能指针的托管对象即 ptr_resource
,交换了本来函数的参数 a ,托管对象 ptr_resource
为空,现在换来了一个有用的,把空的换给了a,于是 a 的资源为空,所以 a 使用资源时就会出现空指针的错误!
2. std::foward
有了前面的讨论,这个就简单一些了,不铺的很开。先看函数原型:
/**
* @brief Forward an lvalue.
* @return The parameter cast to the specified type.
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept {
return static_cast<_Tp&&>(__t);
}
/**
* @brief Forward an rvalue.
* @return The parameter cast to the specified type.
* This function is used to implement "perfect forwarding".
*/
template<typename _Tp>
constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept {
static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument substituting _Tp is an lvalue reference type");
return static_cast<_Tp&&>(__t);
}
有两个函数:
第一个,参数是左值引用,可以接受左值。
第二个,参数是右值引用,可以接受右值。
根据引用折叠的原理,如果传递的是左值,Tp
推断为 string&
,则返回变成static_cast<string& &&>
,也就是 static_cast<string&>
,所以返回的是左值引用。
如果传递的是右值,Tp
推断为 string
或 string&&
,则返回变成static_cast<string&&>
,所以返回的是右值引用。
反正不管怎么着,都是一个引用,那就都是别名,也就是谁读取std::forward
,都直接可以得到std::foward
所赋值的参数。这就是完美转发的基本原理!
/**
* 编译:g++ test_forward.cpp -lpthread -o out
* 执行:./out
* 这是测试代码,不够严谨,仅为了说明std::forward的用途
* 例子的意思是,希望执行一个函数,函数放在子线程执行,函数由业务方随时定义
* */
#include <memory>
#include <iostream>
#include <thread>
#include <unistd.h>
template<typename Closure>
class ClosureTask {
public:
explicit ClosureTask(std::string &&name, Closure &&closure) : name_(std::forward<std::string>(name)), closure_(std::forward<Closure>(closure)) {
}
bool DoTask() {
// 执行Lambda函数
closure_();
return true;
}
private:
typename std::decay<Closure>::type closure_;
std::string name_;
};
// 异步调用,非阻塞
template<typename Closure>
void PostTask(std::string &&name, Closure &&closure) {
// 用 forward 透传 name 与 closure
std::unique_ptr<ClosureTask<Closure>> queueTask(new ClosureTask<Closure>(std::forward<std::string>(name), std::forward<Closure>(closure)));
printf("PostTask\n");
// 启动一个线程执行任务,taskThread 的第二个参数,也是一个 Lambda 表达式
// = 号表示外部的变量都可以在表达式内使用, &queueTask 表示表达式内部要使用该变量
std::thread taskThread([=, &queueTask]() {
printf("start thread\n");
queueTask->DoTask();
printf("thread done\n");
});
taskThread.detach();
}
int main(int argc, char *argv[]) {
printf("start\n");
// 参数2,传递的是Lambda表达式
// Lambda 是最新的 C++11 标准的典型特性之一。Lambda 表达式把函数看作对象
PostTask("TestForward", []() mutable {
// 执行一个任务,任务的内容就在这里写
printf("I want to do something here\n");
});
int a{};
while (a < 10) {
std::cout << a << std::endl;
sleep(1);
a++;
}
return 0;
}