C++万能引用与完美转发

C++万能引用与完美转发

什么是万能引用

如果函数模板形参具备T&&型别,并且T的型别系推导而来,或如果对象使用auto&&声明其型别,则该形参或对象就是个万能引用。

万能引用首先是一个引用,它既可以绑定到左值引用,也可以绑定到右值引用。

先看个例子:

extern "C"{
    int printf(const char * format, ...);
}

template<typename T>
void func(T& val){              //左值引用
    printf("Val = %d\n", val);
}

int main(){
    int a = 520;
    func(a);
    func(521);      //此句编译出错
    return 0;
}

上述代码中,模板函数只能接受一个左值引用。我们想要模板既可以接受一个左值,又可以接受一个右值,应该怎么做?答案是使用万能引用:

extern "C"{
    int printf(const char * format, ...);
}

template<typename T>
void func(T&& val){             //万能引用
    printf("Val = %d\n", val);
}

int main(){
    int a = 520;
    func(a);
    func(521);      
    return 0;
}

此时,若采用右值来初始化万能引用,就会得到一个右值引用。若采用左值来初始化万能引用,就会得到一个左值引用。

区分万能引用和右值引用

如果型别声明并不是精确地具备Type&&形式,或者型别推导并未发生,则Type&&就代表右值引用。

void fun(Obj&& param);                  //右值引用

Obj&& val1 = Obj();                     //右值引用

auto&& val2 = var1;                     //万能引用

template<typename T>
void fun(std::vector<T>&& param);       //右值引用

template<typename T>
void fun(T&& param);                    //万能引用

template<typename T>
void fun(const T&& param);              //右值引用

template<class T>
class vector{
public:
    void push_back(T&& x);              //右值引用
    ...                                 //因为这里的T不是在这里被推导的
    template<class... Args>
    void emplace_back(Args&&... args);  //万能引用
}

template<typename MyTemplateType>
void someFunc(MyTemplateType&& param);  //万能引用
万能引用的本质

万能引用并非一种新的引用类型,其实它是满足下面两个条件的语境中的右值引用:

  • 型别推导的过程会区分左值和右值。T型别的左值推导结果为T&,而T型别的右值则推导为T。
  • 会发生引用折叠

例如:

template<typename T>
void fun(T&& param){
    //如果实参param是左值,T会推导成为T&,即变为T& && param,触发引用折叠,变为T& param
    //如果实参param是右值,T会推导成为T,即变为T&& param
}
引用折叠

上述模板型别推导过程中出现了T& &&这种东西,即“引用的引用”,实际上“引用的引用”在C++中是非法的,编译器使用引用折叠机制来处理这个问题。

引用折叠机制其实很简单:当编译器在引用折叠语境下生成引用的引用时,结果会变成单个引用。如果原始的引用中有任一引用为左值引用,则结果为左值引用。否则,结果为右值引用。

即有:

T& && param  --> T& param
T&& & param  --> T& param
T& & parma   --> T& param
T&& && param --> T&& param
完美转发

先来分析以下代码:

class A{
public:
    A(const A& obj):val(obj.val){
        std::cout<<"A copy constructor."<<std::endl;
    }
    A(A&& obj):val(std::move(obj.val)){
        std::cout<<"A move constructor."<<std::endl;
    }
    A(int _val):val(_val){
        std::cout<<"A common constructor."<<std::endl;
    }
private:
    int val;
};

class B{
public:
    B(const A &obj):a(obj){
        std::cout<<"B copy constructor."<<std::endl;
    }
    B(A&& obj):a(obj){      //这里需要注意的是右值引用其实是一个左值
        std::cout<<"B move constructor."<<std::endl;
    }
private:
    A a;
};

int main(){
    A obj1(5);
    //A common constructor.
    B obj2(obj1);
    //A copy constructor.
    //B copy constructor.
    B obj3(std::move(obj1));
    //A copy constructor.       //因为右值引用是左值,所以A调用的仍然是复制构造函数
    //B move constructor.
    B obj4(5);
    //A common constructor.
    //A copy constructor.
    //B move constructor.
    return 0;
}

将代码稍作修改:

class A{
public:
    A(const A& obj):val(obj.val){
        std::cout<<"A copy constructor."<<std::endl;
    }
    A(A&& obj):val(std::move(obj.val)){
        std::cout<<"A move constructor."<<std::endl;
    }
    A(int _val):val(_val){
        std::cout<<"A common constructor."<<std::endl;
    }
private:
    int val;
};

class B{
public:
    B(const A &obj):a(obj){
        std::cout<<"B copy constructor."<<std::endl;
    }
    B(A&& obj):a(std::move(obj)){      //使用std::move将右值引用转化为右值
        std::cout<<"B move constructor."<<std::endl;
    }
private:
    A a;
};

int main(){
    A obj1(5);
    //A common constructor.
    B obj2(obj1);
    //A copy constructor.
    //B copy constructor.
    B obj3(std::move(obj1));
    //A move constructor.       //使用std::move转为为右值,所以A调用的是移动构造函数
    //B move constructor.
    B obj4(5);
    //A common constructor.     //这里先调用A的普通构造函数生成一个临时对象ta,这是一个右值,即B(A&& obj)中的形参obj
    //A move constructor.       //将临时对象obj移动构造A
    //B move constructor.       //
    return 0;
}

这里我们可以看到虽然A类存在一个接受参数整形val的构造函数,但B中没有接受整形val的构造函数。所以当B接受一个整形val进行构造时,需要先构造出一个A类的临时对象,然后调用一次A移动构造函数,还需调用一次A的析构函数。

有没有办法不生成这个临时对象呢?

你也许会说在B类中再加一个接收整形val的重载构造函数不就好了,就像下面这样:

B(int _val):a(_val){

}

这的确是一种方法,但是这种设计的可扩展性太差。对于那些有多个形参的函数,每个形参都需要一个左值和一个右值,从而重载函数的个数会呈几何级数增长。更为糟糕的是有些函数会有无穷多个形参,而每个形参都可能是左值或右值。

那么最佳的解决办法是什么呢?那就是万能引用加完美转发。

先看代码:

class A{
public:
    A(const A& obj):val(obj.val){
        std::cout<<"A copy constructor."<<std::endl;
    }
    A(A&& obj):val(std::move(obj.val)){
        std::cout<<"A move constructor."<<std::endl;
    }
    A(int _val):val(_val){
        std::cout<<"A common constructor."<<std::endl;
    }
    ~A(){
        std::cout<<"A destructor."<<std::endl;
    }
private:
    int val;
};

class B{
public:
    template<typename T>
    B(T&& param):a(std::forward<T>(param)){         //param是一个万能引用
        std::cout<<"B constructor."<<std::endl;
    }
private:
    A a;
};

int main(){
    A obj1(5);
    //A common constructor.
    B obj2(obj1);
    //A copy constructor.
    //B constructor.
    B obj3(std::move(obj1));
    //A move constructor.
    //B constructor.
    B obj4(5);
    //A common constructor.
    //B constructor.
    return 0;
}

这样问题就解决了,而且代码比之前要简短了好多有木有。

std::forward所做的事情实际上与std::move类似。std::move无条件将实参强制转换为右值,而std::forward是有条件的强制类型转换:仅当其实参是使用右值完成初始化时,它才会执行向右值型别的强制类型转换。

另外需要注意的一点是,针对右值引用的最后一次使用实施std::move,针对万能引用的最后一次使用实施std::forward。否则可能会出问题。

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