c++: 左值、右值、左值引用、右值引用、传参、返回值、转发 等笔记

左值和右值

两个场景:传参和返回

c++是一门值类型语言,在传对象的时候默认使用深拷贝(拷贝整块内存)。例如一个vector,c/go在拷贝时并不拷贝原先放在容器中的
元素,而只是建立指向他们的引用。java的类对象都是别名,必须用.clone()方法。c++则在拷贝时对容器本身(
元信息)包括其内容进行拷贝,通过对=进行重载。

下面说下左值和右值。这是一个很有历史的问题,在c/B语言中都有出现。
最直接的区别是,右值并不能取地址。例如

// 情景1:返回值
S f(){

    S s;
    return s;
}
int main(){
    S a = f(); 
    S*a = &f(); // 报错:taking address of rvalue
    // 但是gcc可以通过-fpermissive来允许这件事。    
}

在上面的情况中,产生了两次拷贝:1. 把f中的临时变量s复制到返回值f()中;2. 把f()复制到变量a中。

考虑也很常见的下面这种情况,即使用临时对象传参:

// 情景2:传参
void f(vector<int> a){

}
int main(){
    f(vector<int> {1,2,3});
}

这种方式的性能比较差。会导致临时—>局部变量的拷贝和两个变量的析构。

为了解决问题:

  1. 由于临时对象是右值,一般常用的 vector const *这样并不能使用。如果使用f(new vector{})会导致不知道什么时候析构的问题。
  2. 于是,c++进行了[强制的右值和const & 的转换],形成了一个左值,即指向那个临时存放的地址。(如果是左值的话这种转换就是自然的)就可以这样写了:
// 情景2 -1
void f(vector<int> const & a){

}
int main(){
    f(vector<int> {1,2,3});
}

使用引用,就出现了一个问题:这个对象什么时候析构?需要保证这个函数内部是不会出现使用析构的变量。实际上这个临时对象在语句结束,即“;”的时候析构,因此在整个执行过程中都指向一个有效对象。在c98里,就这样
解决了拷贝的问题。

再回到情景1,由于右值被允许const &成为左值,即这样的语法是成立的:

const S& s = f();

以现有信息而言,在语句结束之后f()这个临时变量应当被析构。c++为了解决这个左值引用失效的问题,它
允许这样的语句延长对象的生命周期直到左值s的作用域结束。例如:

int main(){
    {
        const S& s = f();
        cout <<"1";
    }
    cout <<"2";
}
// output: 1, destructor, 2;

(当然也有可能不析构,比如f的返回值是一个static变量的引用,就由编译器控制他的析构,编译器不再块结束后进行析构。)

更进一步地,现在有两个动机:

  1. 我们想保存这个右值而不通过一般地赋值完整地拷贝一次。
  2. 另外一个原因是赋值的源在设计上就是很多时候在赋值之后就没用了(比如数据传递)。现在想要修改这个源,比如修改一些计数。

c++11引入了移动语义和右值引用。下面的例子中,要着重看注释中提到的的优先级问题。

//c++11, = 号重载
struct S{
    S &operator=(const S & s){ // 左值、右值都能用,c98所使用的。拷贝赋值
        return *this;
    }
     S &operator=(S & s){
        // 左值引用,传入左值优先匹配这个,现已淘汰
     }
     S &operator=(S && s){
        // 右值引用,传入右值优先匹配这个 ,移动赋值
     }
};

优先级的另一个例子:

string makestring( string && var1, string && var2){
	cout << "1"<<endl;
		return var1+var2;
}
string makestring(const string& a,const string& b){
	cout << "2"<<endl;
	return a+b ;
}
int main(){
	string var1("string1");
	string var2("string2");
	makestring(makestring(var1,var2),makestring(var2,var1)); 
	// output : 2,2,1
}

std::move把左值转为右值,这使得上(3)能被调用,即S&&(s)。
因此,问题2的解决方法就成了:

S s;
S ss;
s = std::move(ss);
析构ss

为什么不使用上(2)代替右值引用,即使临时变量确实有一个地址?这与兼容性有关。
如果这样改后,即对于一般地拷贝赋值a = b;就调用了(2),那么为了不使用移动语义,
必须写成 a=(const S & )b。另外能不能用一个方法完成这个事,比如一个move_from函数?
这也是不合适的,考虑到return S();这种连续两次赋值的情形,无法得到具体的名字来调用。

其他语法

转发引用

在语法层面,c++支持了一种写法-转发引用T&&,
也有人称其为万能引用。它允许左值和右值放入其中,但是要求不能带其他限定,也必须自动推导。
例如:

template<typename T>
void f(T&&t) // const T&&
{
    
}
int main(){
    int i ;
    f(i);   //左值 -> T是一个左值引用int&。T&&是一个左值引用
    f(i+1); //右值 -> T是int。T&&是右值引用
}

此时,T分别被推导为int & 和 int。而(int &)&&是什么?这被称为引用折叠,即如果有左值引用就是左值引用,否则是右值引用。
因此T&&就不会报错。利用模板的推导功能(或者auto)和引用折叠,就形成了转发引用这种语法。也就是说:T&&和auto&&并不像
&&所示是右值引用。

在明确这点后,可以看下move和forward的代码

//std::remove_reference<_Tp>只获得变量的类型,比如S.
template<typename _Tp>  constexpr typename std::remove_reference<_Tp>::type&&  move(_Tp&& __t) noexcept  
{
    return static_cast<typename std::remove_reference<_Tp>::type&&>(__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);
} // 类似于转发引用

复制消除

回到场景1——类似于工厂方法构造对象的时候,我们说会导致两次复制(或者移动,等构造)。c++支持了一种复制消除机制,
不止是编译器层面的支持,更是语义级别的要求。例如:S s = f(),先在f里利用S()构造函数构造一次,再将此变量拷贝构造到返回值处,
再将此变量使用S(S&&)或者S(const S &)构造到s变量中。但是实际执行的时候只有一次默认构造函数f的执行。
这点在c17标准中成为强制使用的。这样两种情况下:

  1. 在 return 语句中,当操作数是一个与函数返回类型相同的类类型的纯右值时
  2. 在对象的初始化中,当初始化器表达式是一个与变量类型相同的类类型的纯右值时

以避免拷贝构造/移动构造。

S f(){
    S a();
    return a;
}
S g(S a){
    return a;
}
S g(){
    return S(); //第一种情况
}
int main(){
    S s = g(f()); // 无法避免
    S s = f(); //第二种情况
    S s = g(); //第一种情况
}

补充:

值:glvalue(广义左值), rvalue

-> lvalue | xvalue -> xvalue(亡值) | prvalue(纯右值)

lvalue包括:

指代变量或函数的表达式、解引用运算符的使用、字符串字面量、返回值类型为左值引用的函数的调用、++a、--a

prvalue包括:

除字符串字面量和用户自定义字面量以外的字面量、取址运算符的使用、内置数学运算符的使用(a + b等)、返回值类型为非引用类型的函数的调用、
lambda表达式、a++、a--

xvalue包括:

返回值类型为右值引用的函数的调用(如std::move())、向右值引用类型的转换、a[n],a是数组;a.m,a是rvalue,m是非引用类型的非静态数据成员、a.*mp,a是rvalue,mp是指向数据成员的指针。

右值和左值并非泾渭分明,因为一个右值转为右值引用后就是一个广义左值也就是一个亡值。

    S() = 123; //不会报错  ,但是永远用不到S().

参考材料

  1. https://www.bilibili.com/video/BV1MP4y1S76m/
  2. https://zhuanlan.zhihu.com/p/405878306
  3. https://cloud.tencent.com/developer/article/1561681
  4. https://zhuanlan.zhihu.com/p/394471591
  5. https://wendeng.github.io/2019/05/14/c++%E5%9F%BA%E7%A1%80/c++11move%E5%92%8Cforword/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值