查看原文
其他

左值、右值、左值引用、右值引用

CPP开发者 2021-07-20

(给CPP开发者加星标,提升C/C++技能)

来源:Coco~567
https://blog.csdn.net/u012198575/article/details/83142419

【导读】:本文主要详细介绍了左值、右值、左值引用、右值引用以及move、完美转发。

左值和右值

左值(left-values),缩写:lvalues

右值(right-values),缩写:rvalues

直接上官网查,我一向倡导自己去懂得原理,而原理都是老外写的,当然我只是针对c++编程语言这样说。

https://msdn.microsoft.com/en-us/library/f90831hc.aspx

翻译:所有的c++表达,不是左值就是右值。

  • lvalues是指存在于单个表达式之外的对象。你可以把左值当成有名字的对象。所有的变量,包括常变量,都是左值。

  • rvalues是一个暂时存在的值存在于单个表达式之内的对象。

有点拗口(难理解),通俗来说就是,左值的生存期不只是这句话,后面还能用到它。

而右值呢,出了这句话就挂了,所以也叫(将亡值)。

它举了一个栗子:

#include <iostream>  
using namespace std;  
int main()  
{  
   int x = 3 + 4;  
   cout << x << endl;  
}  

在以上实例中,很显然,x是左值,3 + 4是右值。

它又举了一个栗子,来说明错误的使用和正确的使用

// lvalues_and_rvalues2.cpp  
int main()  
{  
   int i, j, *p;  
  
   // 正确的使用: 变量是左值 
   i = 7;  
  
   // 错误的使用: 左边的操作 必须是 左值 (C2106)
   7 = i; // C2106  
   j * 4 = 7// C2106  
  
   // 正确的使用: 被间接引用的指针是左值
   *p = i;   
  
   const int ci = 7;  
   // 错误的使用: 左边的操作 是 常量左值 (C3892)
   ci = 9// C3892 
  
   // 正确的使用: 条件操作 返回了左值
   ((i < 3) ? i : j) = 7;  
}  

左值引用、右值引用

左值引用:参考说明书《Lvalue Reference Declarator: &》,网站如下:

https://msdn.microsoft.com/en-us/library/w7049scy.aspx

使用语法:类型 + &(引用符) + 表达式

type-id & cast-expression 

翻译:

你可以把左值引用当成对象的另一个名字,lvalue引用声明由一个可选的说明符列表和一个引用声明符组成。

引用必须初始化,而且不能改变。

一个对象的地址可以 转化成 一种指定类型的指针 或者 转化成 一个 相似类型的引用。意义是相同的。

demo:

char c_val = 'c';
char *ptr = &c_val;
char &r_val = c_val;

不要混淆 取地址 和 引用,当&说明符前面带有类型声明,则是引用,否则就是取地址。

通俗来说 &在 ”=” 号左边的是引用,右边的是取地址。

右值引用:参考说明书《Rvalue Reference Declarator: &&》,网站如下:

https://msdn.microsoft.com/en-us/library/dd293668.aspx

使用语法:类型 + && + 表达式

type-id && cast-expression  

翻译:

Move Semantics:移动语义

右值引用使您能够区分左值和右值。Lvalue引用和rvalue引用在语法和语义上是相似的。

右值引用支持移动语义的实现,可以显著提升应用程序的性能。移动语义允许您编写将资源(例如动态分配的内存)从一个对象传输到另一个对象的代码,移动语义行之有效,因为它允许从程序中其他地方无法引用的临时对象转移资源。

为了实现移动语义,你在类中提供一个动态构造,和可选择的动态赋值运算符(operator=)。拷贝和赋值操作的资源是右值的可以自动调用移动语义。不像缺省的拷贝构造,编译器并不提供缺省的动态构造。

demo:

#include <iostream>  
#include <string>  
using namespace std;  
  
int main()  
{  
   string s = string("h") + "e" + "ll" + "o";  
   cout << s << endl;  
}  

在Visual C++ 2010之前,每个调用 “+”运算符会分配和返回一个新的临时的string对象,

“+”运算符不能从一个string扩展到另一个,因为它不知道string是左值还是右值。如果源字符串都是lvalues,那么它们可能在程序的其他地方被引用,因此不能被修改。通过使用右值引用“+”运算符能够修改那些不能在程序中别处引用的右值,所以现在“+”运算符可以有一个string扩展到另一个。这可以显著减少字符串类必须执行的动态内存分配的数量。

为了更好地理解移动语义,考虑向向量对象插入一个元素的例子。如果超出了vector对象的容量,vector对象必须为其元素重新分配内存,然后将每个元素复制到另一个内存位置,以便为插入的元素腾出空间。当插入操作复制一个元素时,它创建一个新元素,调用copy构造函数将数据从前一个元素复制到新元素,然后销毁前一个元素。移动语义允许您直接移动对象,而不必执行昂贵的内存分配和复制操作。

Perfect Forwarding:完美转发

完美的转发减少了重载函数 避免了转发的问题。转发的问题出现在你写通用函数将引用作为参数,将这些参数由函数调用的时候。

举个例子,如果通用函数将 type const T&作为参数,那么调用函数不能修改参数的值。

如果通用函数 将 type T&作为参数,那么当参数是右值的时候,函数不能调用。

通常来说,为了解决上述的问题,你需要提供重载函数,既要有type const T&参数的函数,也要有type T&参数的函数。

结果呢,重载函数的数量随着参数数量呈指数递增。而右值引用能够使你只用一个函数就能适用于任意数量的参数。

原先的做法如下:

先写出所有适用的通用函数

struct W  
{
  
   W(int&, int&) {}  
};  
  
struct X  
{
  
   X(const int&, int&) {}  
};  
  
struct Y  
{
  
   Y(int&, const int&) {}  
};  
  
struct Z  
{
  
   Z(const int&, const int&) {}  
};  

再将带有不同类型的参数的函数用模板结合起来

template <typename T, typename A1, typename A2>  
T* factory(A1& a1, A2& a2)  
{  
   return new T(a1, a2);  
}  

调用:需要根据适用的类型用相应的指针对接。

当调用的是左值时

int a = 4, b = 5;  
W* pw = factory<W>(a, b);  

当调用的是右值时。但是,下面的示例中没有包含对工厂函数的有效调用,因为工厂将可修改的lvalue引用作为其参数,但是它是通过使用右值调用的:

这里要注意的是const int &是lvalue 而不是 rvalue

而2是rvalue,函数会编译不过。

Z* pz = factory<Z>(22);  

为了解决这类问题,需要将模板函数修改成如下形式,右值引用可以适用const T& 和 T&形式的参数:

template <typename T, typename A1, typename A2>  
T* factory(A1&& a1, A2&& a2)  
{  
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));  
}  

经过上述修改,均可以调用,如下图代码所示:

int main()  
{  
   int a = 4, b = 5;  
   W* pw = factory<W>(a, b);  
   X* px = factory<X>(2, b);  
   Y* py = factory<Y>(a, 2);  
   Z* pz = factory<Z>(22);  
  
   delete pw;  
   delete px;  
   delete py;  
   delete pz;  

除了上述所示的右值引用,还有额外的强大功能:

Additional Properties of Rvalue References

1、可以通过重载函数,调用左值或右值参数。具体来讲,你可以通过重载区分出无法修改的对象(const T&):左值和可修改的临时对象(T &):右值。

demo:

#include <iostream>  
using namespace std;  
  
// A class that contains a memory resource.  
class MemoryBlock  
{
  
   // TODO: Add resources for the class here.  
};  
  
void f(const MemoryBlock&)  
{  
   cout << "In f(const MemoryBlock&). This version cannot modify the parameter." << endl;  
}  
  
void f(MemoryBlock&&)  
{  
   cout << "In f(MemoryBlock&&). This version can modify the parameter." << endl;  
}  
  
int main()  
{  
   MemoryBlock block;  
   f(block);          //左值引用调用
   f(MemoryBlock());  //右值引用调用
}  

调用结果如下:

In f(const MemoryBlock&). This version cannot modify the parameter.  
In f(MemoryBlock&&). This version can modify the parameter. 

2、再看下一个demo:与上述demo不同的是

f函数中参数命名了block,值得一提的是在右值引用中,若参数带有名字则视为左值,因为该参数可以在别处调用。

#include <iostream>  
using namespace std;  
  
// A class that contains a memory resource.  
class MemoryBlock  
{
  
   // TODO: Add resources for the class here.  
};  
  
void g(const MemoryBlock&)   
{  
   cout << "In g(const MemoryBlock&)." << endl;  
}  
  
void g(MemoryBlock&&)   
{  
   cout << "In g(MemoryBlock&&)." << endl;  
}  
  
MemoryBlock&& f(MemoryBlock&& block)  
{  
   g(block);  
   return block;  
}  
  
int main()  
{  
   g(f(MemoryBlock()));  
}  

调用结果如下:在这个例子中main函数传递了右值给了f函数,f函数将参数block视为了左值,调用了左值引用的函数g(const MemoryBlock&)   ,接着返回一个右值对象,然后调用了右值引用函数g(MemoryBlock&&) 。

In g(const MemoryBlock&).  
In g(MemoryBlock&&).  

3、右值引用中还可以通过static_cast将左值转成右值,进行右值引用函数调用

demo:

#include <iostream>  
using namespace std;  
  
// A class that contains a memory resource.  
class MemoryBlock  
{
  
   // TODO: Add resources for the class here.  
};  
  
void g(const MemoryBlock&)   
{  
   cout << "In g(const MemoryBlock&)." << endl;  
}  
  
void g(MemoryBlock&&)   
{  
   cout << "In g(MemoryBlock&&)." << endl;  
}  
  
int main()  
{  
   MemoryBlock block;  
   g(block);  
   g(static_cast<MemoryBlock&&>(block));  
}

调用结果如下:static_cast<MemoryBlock&&>(block)将block转成了右值。

In g(const MemoryBlock&).  
In g(MemoryBlock&&).  

4、模板函数可以推断出模板参数类型,进而使用“引用折叠”规则

#include <iostream>  
#include <string>  
using namespace std;  
 
template<typename T> struct S;
template<typename T> struct S<T&>
{

    static void print(T& t)
    
{
        cout << "print<T&>: " << t << endl;  
    }
};
template<typename T> struct S<const T&> {  
   static void print(const T& t)  
   
{  
      cout << "print<const T&>: " << t << endl;  
   }  
};  
  
template<typename T> struct S<T&&> {  
   static void print(T&& t)  
   
{  
      cout << "print<T&&>: " << t << endl;  
   }  
};  
  
template<typename T> struct S<const T&&> {  
   static void print(const T&& t)  
   
{  
      cout << "print<const T&&>: " << t << endl;  
   }  
};  
 
template <typename T> void print_type_and_value(T&& t)   
{  
   S<T&&>::print(std::forward<T>(t));  

 
const string fourth() return string("fourth"); } //这个函数为了返回const string类型的 "fourth" 即const + 右值
 
int main()  
{  
   string s1("first");  //左值调用
   print_type_and_value(s1);   
  
   const string s2("second");  //const 左值
   print_type_and_value(s2);  
  
   print_type_and_value(string("third"));  //右值
  
   print_type_and_value(fourth());  //const 右值
}  

调用结果:

print<T&>: first  
print<const T&>: second  
print<T&&>: third  
print<const T&&>: fourth  

查看一下引用折叠规则:



Expanded typeCollapsed type
T& &T&
T& &&T&
T&& &T&
T&& &&T&&

对照代码:

调用print_type_and_value(T&& t)

例如first 是左值引用 则把t 参数看成T& 类型

即T变成 T& 这里T 是 string

则T 变成了 String&

即如下代码所示:

print_type_and_value<string&>(string& && t) 

forward的函数实现如下:将上述类型T& 转发成 T& &&

T& && forward(remove_reference<T&>::type& a) noexcept
{
  return static_cast<T& &&>(a);

进而通过引用折叠规则(表的第二条)对应的Collapsed type变成 了 T&即调用函数struct S<T&>。

总结:

右值引用将左值与右值区分开来。它们可以帮助您通过消除不必要的内存分配和复制操作来提高应用程序的性能。它们还使您能够编写接受任意参数的函数的一个版本,并将其转发给另一个函数,就好像直接调用了另一个函数一样

- EOF -


推荐阅读  点击标题可跳转

1、无锁队列的实现

2、C++ std::function技术浅谈

3、做引擎开发,更需要深入 C++ 内存管理


关于左值、右值、左值引用、右值引用,欢迎在评论中和我探讨。觉得文章不错,请点赞和在看支持我继续分享好文。谢谢!


关注『CPP开发者』

看精选C++技术文章 . 加C++开发者专属圈子

↓↓↓


点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存