C++的std::move与std::forward原理大白话总结
阅读大型的C++开源项目代码,基本逃不过std::move和std::forward,例如webRTC。所以搞懂其原理,很有必要。网络上已有不少文章介绍(见@参考),但是比较分散,所以我把自认为的关键点,加上一些自己的想法,提取总结一下。1. std::move别看它的名字叫move,其实std::move并不能移动任何东西,它唯一的功能是将一个左值/右值强制转化为右值引用,继而可以通过右值引用使
阅读大型的C++开源项目代码,基本逃不过std::move和std::forward,例如webRTC。
所以搞懂其原理,很有必要。
网络上已有不少文章介绍(见@参考),但是比较分散,所以我把自认为的关键点,加上一些自己的想法,提取总结一下。
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); //15是右值, 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&& param) {
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;
func(1); // 传递参数是右值 T推导成了int, 所以是int&& param, 右值引用
func(x); // 传递参数是左值 T推导成了int&, 所以是int&&& param, 折叠成 int&,左值引用
func(getInt());// 参数getInt是右值 T推导成了int, 所以是int&& param, 右值引用
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::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
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 <stdio.h>
#include <unistd.h>
#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> gPtrC(new TestC(2, 3));
//初始化也可以写成如下这一句
//std::unique_ptr<TestC> gPtrC = std::make_unique<TestC>(2, 3);
TestFunc(std::move(gPtrC));
//执行下面这一句会崩溃,因为gPtrC已经没有控制权
gPtrC->print();
return 0;
}
输出:
construct TestC
TestFunc called
print a 2 b 3
destruct TestC
从日志可见,只有一次构造。
这种类型的代码,在大型开源项目,如webRTC,随处可见。下次看到了不用纠结,不用关心细节了。只要直到最后拿到unique_ptr的变量(左值)有控制权就行了。
1.6 再说转移对象控制权
从@1.5.2的例子,看到std::move(gPtrC)之后,执行gPtrC->print();会崩溃?这是为什么呢?
其是,不全部是std::move的功劳,还需要使用方,即unique_ptr配合才行。
请看这篇文章:
https://blog.csdn.net/newchenxf/article/details/116274506
当调用
TestFunc(std::move(gPtrC));
这TestFunc的参数ptrC要初始化,调用的是operator=,我把关键代码截取如下:
class unique_ptr
{
private:
T * ptr_resource = nullptr;
...
unique_ptr& operator=(unique_ptr&& move) noexcept
{
move.swap(*this);
return *this;
}
// swaps the resources
void swap(unique_ptr<T>& resource_ptr) noexcept
{
std::swap(ptr_resource, resource_ptr.ptr_resource);
}
从函数看,执行完赋值后,智能指针的托管对象,即ptr_resource,交换了。
本来函数的参数ptrC,托管对象ptr_resource为空,现在换来了一个有用的,把空的,换给了gPtrC,于是gPtrC的资源为空,所以gPtrC使用资源时,也遇到空指针的错误了!
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所赋值的参数。
这就是完美转发
的基本原理!
2.1 std::forward的常用例子
阅读一些大型项目代码,发现std::forward常用于Lambda函数的完美转发。
我从项目中抽取了代码,来说明其使用方式。具体见下面。
/**
* 编译:g++ test_forward.cpp -lpthread -o out
* 执行:./out
*
* 这是测试代码,不够严谨,仅为了说明std::forward的用途
* 例子的意思是,希望执行一个函数,函数放在子线程执行,函数由业务方随时定义
* */
#include <stdio.h>
#include <unistd.h>
#include <memory>
#include <iostream>
#include <thread>
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() {
closure_();//执行Lambda函数
return true;
}
private:
typename std::decay<Closure>::type closure_;
std::string name_;
};
// 异步调用,非阻塞
template <typename Closure>
void PostTask(std::string &&name, Closure &&closure)
{
std::unique_ptr<ClosureTask<Closure>> queueTask(
//用forward透传name
new ClosureTask<Closure>(std::forward<std::string>(name),
//用forward透传closure
std::forward<Closure>(closure)));
printf("PostTask\n");
//启动一个线程执行任务,taskThread的第二个参数,也是一个Lambda表达式
std::thread taskThread([=, &queueTask]() {//=号表示外部的变量都可以在表达式内使用, &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");
});
return 0;
}
参考
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)