文章目录
一、左值和右值
什么是左值、右值?
- 一般化左值(glvalue): 可寻址的表达式,即可使用&操作符的表达式
- 纯右值(prvalue): 只读表达式,即不可使用&操作符的表达式,一般为临时值
- xvalue: 延长了生命周期的表达式,即右值引用
- lvalue :等号左边的值称为左值
- rvalue :等号右边的值称为右值
总结:
- 左值右值是指一个表达式(当然这个表达式可以仅是一个简单的变量),区分左值和右值是看表达式能否使用&操作符。
- 左值引用和右值引用是指左值或者右值的型别,左值的型别可以为左值引用,也可以为右值引用。右值的型别可以为右值引用,但不能为左值引用,因为左值引用仅能引用左值(const修饰的左值引用可以引用右值,因为函数参数重const修饰的参数,编译器会为传入的右值创建临时变量,所以cosnt修饰的左值引用其实是引用的这个临时变量)。右值引用仅能引用纯右值。
二、左值引用和右值引用
引用是一种类型
左值引用:普通的引用
右值引用:可以延长临时对象的生命周期
注意:1)引用是一种型别的修饰;2)没有引用的指针;3)左值引用一定是左值;4)没有引用的指针(因为引用取地址实际为引用所修饰的型别的地址,而非引用的地址),但有指针的引用;
c++禁止比特的引用。std::vector的operator[]返回的不是bool&是因为std::vector做过特化,用一种压缩形式来表示其特有的bool元素,每个bool元素用一个比特来表示。c++禁止比特的引用,所以std::vector做了一个“隐式”代理
三、移动构造
四、型别推导
1. 模版型别推导
模版形参中带引用或者指针符号时说明模版函数期望实参是一个引用或者指针,因此编译器会忽略传入的实参中包含的引用或指针,当左值实参传入万能引用时,编译器又会通过引用折叠规则,为实参添加引用符号。
模版中形参不带引用或指针符号时,说明模版函数期望实参到形参时按值传递,因此,编译器会忽略实参中的cv和引用指针符号。
模版型别推导过程中T不会被推导为右值引用T&&
template<typename T>
void f(T param);
const char* const ptr = "exception";
f(ptr); //ParamType的型别为const char*,T的型别为const char*
请看下述函数模版代码:
template<typename T>
void f(ParamType param);
调用语句形式为:
f(expr)
在编译期间,编译器会根据expr推导出T的型别和ParamType的型别。
推导规则如下:
- 情况1:ParamType 是个指针或引用,但不是个万能引用
1.若expr具有引用类型,则忽略引用部分;
2.对expr的型别和ParamType的型别进行模式匹配,确定T的型别;
模版形式如下:
//1)ParamType为左值引用,仅能接收左值
templae <typename T>
void f(T& param);
//2)ParamType为右值引用,仅能接收右值
templae <typename T>
void f(const T&& param);
templae <typename T>
void f(const std::vector<T>&& param);
//3)ParamType为常引用
templae <typename T>
void f(const T& param);
//4)ParamType为引指针
templae <typename T>
void f(T* param);
int x = 27;
const int cx = x;
const int& rx = x;
int&& rrx = 33;
//1)模版为左值引用情形
f(x); //param的型别(ParamType)为int&,T的型别为int
f(cx);//param的型别(ParamType)为const int&,T的型别为const int
f(rx);//param的型别(ParamType)为const int&,T的型别为const int
f(rrx);//param的型别(ParamType)为int&,T的型别为int
//2)模版为右值引用情形
f(88);//param的型别(ParamType)为int&&,T的型别为int
f(std::move(x));//param的型别(ParamType)为int&&,T的型别为int
//3)模版为常引用情形
f(x); //param的型别(ParamType)为const int&,T的型别为int
f(cx);//param的型别(ParamType)为const int&,T的型别为int
f(rx);//param的型别(ParamType)为const int&,T的型别为int
//4)模版为指针情形
const int* px = &x;
f(&x); //param的型别(ParamType)为int*,T的型别为int
f(px);//param的型别(ParamType)为const int*,T的型别为const int
总结:
- expr忽略引用修饰
- ParamType若不带cv修饰,则expr保留cv修饰,若带cv修饰,则expr忽略cv修饰
- 注意:右值引用形参仅能接受右值,左值引用形参仅能接受左值
- 为什么要忽略引用修饰?
模版形参声明为左值/右值引用说明编程者的意图是该模版函数想要接收一个左值/右值的实参,形参需要为左值/右值引用。所以编译器在在推导型别是会忽略实参中的引用。
情况2:ParamType是个万能引用
1.若expr是个左值,则T和ParamType都会被推导为左值引用。这是在模版型别推导中,T被推导为引用型别的唯一情形。
2.若expr是个右值,则同情况1的右值引用
template<typename T>
void f(T&& param);
int x = 27;
const int cx = x;
const int& rx = x;
//左值
f(x); //param的型别(ParamType)为int&,T的型别为int&
f(cx); //param的型别(ParamType)为const int&,T的型别为const int&
f(rx); //param的型别(ParamType)为const int&,T的型别为const int&
//右值
f(27); //param的型别(ParamType)为int&&,T的型别为int
情况3:ParamType既非指针也非引用
当ParamType既非指针也非引用,则实参到形参按值传递
1.若expr具有引用型别,则忽略其引用部分
2.若expr具有cv修饰,也忽略之
template<typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& crx = x;
f(x); //param的型别(ParamType)为int,T的型别为int
f(cx); //param的型别(ParamType)为int,T的型别为int
f(crx);//param的型别(ParamType)为int,T的型别为int
数组实参和函数实参
数组的型别: const char name[] = "12345567890";
型别为:const char [10]
函数的型别: void SomeFunc(int,double);
型别为:void (int,double)
当函数或数组作为模版函数的实参传递时,若按值传递,则形参型别会退化为:const char*
和void (*)(int,double)
。若按引用传递形参型别则不会发生型别退化,其型别分别为:const char [10]
和void (&)(int,double)
。
2.auto型别推导
- 当不用大括号初始化时,auto型别推导和模版推导规则一致
- 当使用大括号初始化时,auto型别推导会假定用大括号括起的初始化表达式代表一个std::intializer_list,模版型别推导却不会。
- c++11支持在函数返回值或lambda表达式的形参中使用auto,意思是使用模版型别推导而非auto型别推导,因此不能使用大括号作为返回值,或者大括号列表作为实参,会导致编译不通过
c++11使用auto作为返回值时,返回值尾序法指定返回值型别
函数名字之前的auto和型别推导没有任何关系,只是为了说明C++11中的返回值型别尾序语法。返回值型别将在参数列表之后(即->之后)。
尾序语法的好处在于:在指定返回值型别时可以使用函数形参。
返回值型别先序语法会因为形参还未声明导致无法使用函数形参。
template<typename Container,typename Index>
auto f(Container&& c,Index i) ->dedltype(std::forward<Container>(c)[i]){
return std::forward<Container>(c)[i];
}
- 优先使用auto,当auto不能满足要求时使用显示指定
1)“隐形”的代理型别可以导致auto根据初始化表达式推导出错误的型别。
2)带显示型别的初始化物习惯用法强制auto推导出你想要的型别
什么是隐形的代理型别?
std::vector的operator[]返回的是一个std::vector::reference型别的对象而非bool&,std::vector的其他实例化的operator[]的返回值都是对应型别的引用。
如何识别隐形的代理型别?
1)接口文档;2)头文件中的函数签名;
3.decltype型别推导
- 绝大多数情况下,decltype会得出变量或表达式的型别而不做任何修改
- 对于型别为T的左值表达式,除非该表达式仅有一个名字,decltype总是得出T&
- c++14支持decltype(auto),和auto一样,它会从其初始化表达式出发来推导型别,但是它的型别推导使用的是decltype的规则。即将auto的模版型别推导规则指定为decltype推导规则
为什么返回值要指定为decltype推导规则?
因为不指定,则auto使用的是模版推导规则,模版推导规则可能会忽略cv或引用修饰
五、引用折叠
组合: 引用有两种:左值引用和右值引用。所以有四种引用组合:左值-左值(& &)、左值-右值(& &&)、右值-左值(&& &)、右值-右值(&& &&)。意思为:左值引用的左值引用、左值引用的右值引用、右值引用的左值引用、右值引用的右值引用。前面的引用符号可以看作一种型别,后面的引用符号看作该型别的引用。
规则: 如果任一引用为左值引用,则结果为左值引用。否则(即两个皆为右值引用),结果为右值引用。
引用的引用允许出现的四种语境: 模版实例化、auto型别生成、创建和运用typedef和别名声明、以及decltype。
六、函数的返回值
- 函数若返回左值引用,则函数的返回值一定是左值。因为左值引用一定是左值。
- 函数返回值为其他型别时,如,右值引用或者是非引用,则函数返回值为右值。
- 函数返回值优化(RVO)
编译器若要在按值返回的函数里省略对局部对象的复制(或者移动),则需要满足两个前提条件:1)局部对象型别和函数返回值型别相同;2)返回的就是局部对象本身;
七、std::move和sdt::forward
- std::move
1)std::move强制转换为右值
std::move的一种能够完成任务的实现
template <class T>
typename remove_reference<T>::type&& move(T&& param)
{
typedef typename remove_reference<T>::type _Up;
return static_cast<_Up&&>(param);
}
推导后的param的类型为T&(实参为左值时)或T(实参为右值时)。返回时将param去除引用后强制转换为T&&型别,最终的返回结果为T&&型别的右值。
std::move为什么要返回右值引用类型而不是非引用类型?
返回右值引用类型可以确保返回值必定是右值,并且移动构造的形参类型为右值引用类型。若返回值为非引用类型,则无法应用于移动构造了。
2)std::move转换为右值时,若保留了常量性const,则不会调用移动构造函数,而是调用复制构造函数,因为移动构造不接受常量型别的参数,复制构造却可以。
- std::forward
引用折叠是使std::forward得以运作的关键。
std::forward使用时需要具化:std::forward<T>();
为什么要指定T?
因为std::forward的参数类型为:typename std::remove_reference::type&,移除了引用修饰后变成了确定的左值引用,T的型别不能再由实参推导而来,因此需要在调用函数时指定T。参数的类型为什么不能为T&?因为参数的类型为T&时,涉及到的类型推导,且推导的结果总为T&,不能达到区分左右值的目的。
当模版函数中的参数不能由实参推导时,模版函数调用需要指定T。
template<typename T>
void f(typename std::remove_reference<T>::type& param){
//T tRemove = param;
T tRemove = int();
if (std::is_same<decltype(tRemove),int>::value) {
std::cout << "----: tRemove is int" << std::endl;
}
else if (std::is_same<decltype(tRemove), int&>::value) {
std::cout << "----: tRemove is int&" << std::endl;
}
else if (std::is_same<decltype(tRemove), int&&>::value) {
std::cout << "----: tRemove is int&&" << std::endl;
}
}
//call f
int a1;
int& a2 = a1;
int&& a3 = 888;
f<int>(a1); //output : ----: tRemove is int
//f<int&>(a2); //output : ----: tRemove is int&
f<int&&>(a3); //output : ----: tRemove is int&&
std::forward的一种能够完成任务的实现
template<typename T>
T&& std::forward(typename std::remove_reference<T>::type& param) {
return static_cast<T&&>(param);
}
//eg:在万能引用中使用std::forward
template<typename T>
void f(T&& param){
...
someFunc(std::forward<T>(param));//假设someFunc是一个函数
}
若传递给函数f的实参是个int型左值,则std::forward被推导为:
int& && std::forward(typename std::remove_reference<int&>::type& param) {
return static_cast<int& &&>(param);
}
//推导后的结果为:
int& std::forward(int& param) {
return static_cast<int&>(param);
}
强制转换为左值,其实什么也不做,结果仍为左值。
若传递给函数f的实参是个int型或int&&右值,则std::forward被推导为:
//传递int型别右值
int&& std::forward(typename std::remove_reference<int>::type& param) {
return static_cast<int&&>(param);
}
//推导后的结果为:
int&& std::forward(int& param) {
return static_cast<int&&>(param);
}
八、区分右值引用和万能引用
万能引用的条件:
1) T&&
2)T的型别由推导而来
加上const修饰,则称为右值引用
template <typename T>
void f(const T&& param); //param是个右值引用