C++的std::move与std::forward原理大白话总结

阅读大型的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 
#include 
#include 
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::type为string 
//整个std::move被实例如下
string&& move(string&& t) //可以接受右值
{
    return static_cast<string&&>(t);  //返回一个右值引用
}

显而易见,用static_cast,返回的一定是个右值引用。

综上,std::move基本等同于一个类型转换:static_cast(lvalue);

即,输入可以是左值,右值,输出,是一个右值引用。

1.5 std::move的常用例子

1.5.1 用于vector添加值

以下是一个经典的用例:


//摘自https://zh.cppreference.com/w/cpp/utility/move
#include 
#include 
#include 
#include 
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 
#include 
#include 
#include 

/***** 类定义开始****/
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 gPtrC = std::make_unique(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,也就是static_cast,所以返回的是左值引用。

如果传递的是右值,Tp推断为string或string&&,则返回变成
static_cast,所以返回的是右值引用。

反正不管怎么着,都是一个引用,那就都是别名,也就是谁读取std::forward,都直接可以得到std::foward所赋值的参数。

这就是完美转发的基本原理!

2.1 std::forward的常用例子

阅读一些大型项目代码,发现std::forward常用于Lambda函数的完美转发。
我从项目中抽取了代码,来说明其使用方式。具体见下面。

/**
 * 编译:g++ test_forward.cpp -lpthread -o out
 * 执行:./out
 * 
 * 这是测试代码,不够严谨,仅为了说明std::forward的用途
 * 例子的意思是,希望执行一个函数,函数放在子线程执行,函数由业务方随时定义
 * */
#include 
#include 
#include 
#include 
#include 

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;
}

参考

c++ 之 std::move 原理实现与用法总结
[c++11]我理解的右值引用、移动语义和完美转发

你可能感兴趣的:(C++)