目录

C++右值语义的基石——完美转发


什么是完美转发?

熟悉现代C++语法的都应该清楚,C++把变量分为左值和右值,为了实现对资源的转移而不是拷贝,右值和对应的移动构造函数应运而生,但我们发现,很多时候我们并不能把左值和右值精确的传递给对应版本的函数进行处理,比如下面一个简单的代码,你会发现即使我们把函数的参数类型设置为右值引用,但当拿它去调用对应的构造函数时,它给出的竟然是拷贝构造!故这个转发还不够完美!

#include<iostream>
using namespace std;
class test{
public:
    test() = default;
    test(test&& p){
        cout<<"move construct call"<<endl;
    }
    test(const test& p){
        cout<<"copy construct call"<<endl;
    }
};
void test_fun(test&& p){
    test q(p);
    return;
}

int main() {
    test_fun(test());
    return 0;
}

为什么会出现这种情况呢?

因为无论传入的形参是左值还是右值,对于函数内部来说,形参既有名称又能寻址,因此它都被认为是左值

如何实现完美转发?

实现完美转发很简单,我们在现代C++中只需要 forward<T> 这个模板函数即可完成,其实际原理就是利用的 C++11 模板中提供的折叠引用的语法,最终达到的效果就是,把参数的类型强制转换为它该有的类型,是左值就转为左值,是右值就转为右值,从而实现该调用哪个版本的函数就调用哪个版本的函数,不再只被认定为右值了!

先前的代码可以如此实现完美转发:

#include<iostream>
using namespace std;
class test{
public:
    test() = default;
    test(test&& p){
        cout<<"move construct call"<<endl;
    }
    test(const test& p){
        cout<<"copy construct call"<<endl;
    }
};
void test_fun(test&& p){
    test q(forward<test>(p)); //修改的地方
    return;
}

int main() {
    test_fun(test());
    return 0;
}

任何模板库都离不开完美转发

其实现在只要是C++的模板库,没有哪个是不用完美转发的,同时完美转发的问题也是产生自模板,而 forward 函数的实现其实也不是什么难事,实际就是利用 C++11 对模板提供的万能折叠语义:

  • 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
  • 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。

以下为一个简单的利用完美转发设计的创建工厂:

#include<iostream>
#include <memory>

using namespace std;
class test{
public:
    test() = default;
    test(int&& arg):m_iData(arg){
        cout<<"move construct call"<<endl;
    }
    test(const int& arg):m_iData(arg){
        cout<<"copy construct call"<<endl;
    }
private:
    int m_iData;
};

template<typename T,typename Arg>
//不直接用T&&的原因在于,如果只使用一个模板参数会导致factory参数无法获得万能引用的效果
shared_ptr<T> factory(Arg&& arg){
    return shared_ptr<T>(new T(forward<Arg>(arg)));//使用完美转发调用正确的构造函数
}

int main() {
    int val = 5;
    auto p1 = factory<test>(val);
    auto p2 = factory<test>(5);
    return 0;
}

std::forward的实现原理

gcc 的源代码实现如下:

template<typename _Tp>
constexpr _Tp &&
forward(typename std::remove_reference<_Tp>::type &__t) noexcept { return static_cast<_Tp &&>(__t); }

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&_Tp&& 但最终都是通过 static_cast + 折叠引用的特性来实现强制转化的。也就是简单的强转而已!

收获

在设计模板库的时候,如果需要根据左值右值语义比较清晰的实现转发,一定要用forward,否则参数只会被当作左值!