C++范型二:右值引用

为类所设计的转移语义拷贝构造函数转移语义赋值运算符使得临时对象有了将资源直接转移给另一个对象的能力,从而避免了内存分配、资源拷贝等深拷贝过程

作为注重效率的模板,当然要引入右值引用及相关技术,其成果就是参数完美转发模板

右值引用

左值和右值
左值代表一块存储空间,可以接收和保存数据,而右值仅代表数据,是一个临时对象

在C++11之前
左值可以定义两种引用,即T& t=lvalue;const T& t=lvalue;
对于右值,仅定义了一种常引用,即const T& t=rvalue;

C++11为了支持转移语义,提出新的数据类型:右值引用T&& t=rvalue;

右值引用的应用

转移语义

类的拷贝构造函数和赋值运算符重载,这两类函数具有资源控制权转移功能,所以称这类函数可以转移语义

class:
  Foo(Foo&& r){}

main:
  Foo foo(func());

转移函数move()

看到了右值引用的好处,左值也想借鉴
C++11推出了函数T&& move(T&);,接收一个参数,返回该变量的右值引用

move()使用得当,效果是巨大的,如STL中的数据交换函数std::swap()

void swap(T& a, T& b){
    T tmp= move(a); // 调用转移语义拷贝构造函数
    a= move(b);
    b= move(tmp);
}

T是一个包含大量资源的类,那这个操作将会大大提高效率

参数完美转发模板

参数完美转发问题

函数模板中调用外部函数,此时需要用模板传递参数,该行为称为参数转发
在参数转发时要求:一不能改变参数特性,二不能产生额外开销
C++11之前,模板参数转发问题一直没有得到很好的解决,主要原因是右值参数经模板转发后变成左值

void func(int v){}

template<typename T>
void tmp(T a){ func(a); }

int main(){
    int x=0;
    tmp(x);
    return 0;
}

该转发是不完美的,因为是值传递,在转发过程中需要创建临时对象并对数据进行拷贝

如下代码,改成引用传递

void func(int v){}

template<typename T>
void tmp(T& a){ func(a); }

int main(){
    int x=0;
    tmp(x);
    tmp(10); // 右值参数
    return 0;
}

该转发仍不是完美转发,因为不能进行右值参数转发,若采用左值和右值都能接收的常量左值引用const T&,则会不满足某些应用场景

可知,能完美转发的必须是右值引用

void func(int& v){}

template<typename T>
void tmp(T&& a){ func(a); }

int main(){
    int x=0;
    tmp(x);
    tmp(10); // 右值参数
    return 0;
}

C++11在推出右值引用后,做了两件事
1.制定了相应的引用类型&推导规则
2.开发与move()功能类似的forward(),以将被模板转换成左值的右值参数再转换回来

引用符&折叠规则

引用符&折叠规则,即模板参数类型推导规则
为了使编译器能正确处理多个引用符&连用的参数类型,C++11为模板定义了新的引用类型推导规则
支持C++11的编译器在模板的参数推导中一旦发现具有多个引用符的表达式时,便会按照新定义的规则将多余的引用符截掉

引用符的折叠规则reference collapsing rule

- - 折叠结果
T& T& T&
T&& T& T&
T& T&& T&
T&& T&& T&&

如下面定义多个引用符的情况

typedef int& LR;
typedef int&& RR;

int a= 10;
LR b= a;

LR& c= a;  // & &叠加
LR& c= 10; // 编译错误

LR&& f= a;  // & &&叠加
LR&& f= 10; // 编译错误

RR& g= a;  // && &叠加
RR& g= 10; // 编译错误

RR d= 10;   // 右值引用
RR&& e= 10; // && &&叠加
RR&& e= a;  // 编译错误

forward()函数

对于以右值引用T&&作为参数的函数模板,其参数推导有以下两条特殊规则

实参类型 模板形参 类型推导结果
左值lvalue T&& T&
右值rvalue T&& T
void func(int& v){ std::cout<<"call &"; }
void func(int&& v){ std::cout<<"call &&"; }

// 转发模板
template<typename T>
void tmp(T&& a){
    func(a);
}

int main(){
    int x=1;
    int& y=x;
    tmp(x);   // call &
    tmp(y);   // call &
    tmp(100); // call &
    return 0;
}

可以看出,当传入右值100时,没能正确调用预期函数,因为模板将右值转换成了左值,正确做法如下:

template<typename T>
void tmp(T&& a){
    if(a is rvalue){
        func(move(a));
    }else{
        func(a);
    }
}

move()功能单一,仅能将T&类型转为T&&,所以为了避免if-else结构,可使用下面的定义

template<typename T>
void tmp(T&& a){
    func(static_cast<T&&>(a));
}

由引用符折叠规则可知,此时能得到正确的参数转发

使用static_cast<>保证了参数的输入类型与函数的参数类型完全一致
收到的是右值,发到函数的一定是右值,收到的是左值,发到函数的一定是左值
其实,static_cast<>仅对参数为右值时有用,当模板输入的参数为左值时,类型被推导为T&&&,即T&

为了区别于move()static_cast<>,并使其更具有语义性
C++11将static_cast<>封装成函数模板std::forward(),所以可以用下面的代码实现完美转发

template<typename T>
void tmp(T&& a){
    func(forward<T>(a));
}

一个完美转发函数模板的应用实例

#include<iostream>
void func(int& x){std::cout<<"lvalue"<<std::endl;}
void func(const int& x){std::cout<<"const rvalue"<<std::endl;}
void func(int&& x){std::cout<<"rvalue"<<std::endl;}
void func(const int&& x){std::cout<<"const rvalue"<<std::endl;}

// 完美转发模板
template<typename T>
void funcT(T&& a){
    func(std::forward<T>(a));
}

int main(){
    // 右值
    funcT(10); // rvalue

    // 左值
    int a;
    funcT(a); // lvalue

    // 右值
    funcT(std::move(a)); // rvalue

    // 左值常量
    const int b=8;
    funcT(b); // const lvalue

    // 右值常量
    funcT(std::move(b)); // const rvalue

    return 0;
}

根据模板函数的参数(T&&类型)推导规则和引用符折叠规则,实现了参数的完美转发,解决了C++长久以来为人诟病的临时对象效率问题

在支持C++11的STL库中,有大量完美转发的应用,如make_pair()make_unique()

posted @ 2024-10-31 17:37  sgqmax  阅读(12)  评论(0编辑  收藏  举报