服务器之家:专注于VPS、云服务器配置技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - C/C++ - C++ 右值语义相关总结

C++ 右值语义相关总结

2021-10-25 13:54后端技术小屋 C/C++

这篇文章主要介绍了C++ 右值语义的的相关资料,帮助大家更好的理解和学习使用c++,感兴趣的朋友可以了解下

在现代C++的众多特性中,右值语义(std::move和std::forward)大概是最神奇也最难懂的特性之一了。本文简要介绍了现代C++中右值语义特性的原理和使用。

1 什么是左值,什么是右值?

?
1
2
int a = 0;  // a是左值,0是右值
int b = rand(); // b是左值,rand()是右值

直观理解:左值在等号左边,右值在等号右边

深入理解:左值有名称,可根据左值获取其内存地址,而右值没有名称,不能根据右值获取地址。

2 引用叠加规则

左值引用A&和右值引用A&&可相互叠加, 叠加规则如下:

?
1
2
3
4
A& + A& = A&
A& + A&& = A&
A&& + A& = A&
A&& + A&& = A&&

举例说明,在模板函数void foo(T&& x)中:

如果T是int&类型, T&&为int&,x为左值语义
如果T是int&&类型, T&&为int&&, x为右值语义
也就是说,不管输入参数x为左值还是右值,都能传入函数foo。区别在于两种情况下,编译器推导出模板参数T的类型不一样。

3 std::move

3.1 What?

在C++11中引入了std::move函数,用于实现移动语义。它用于将临时变量(也有可能是左值)的内容直接移动给被赋值的左值对象。

3.2 Why?

知道了std::move是干什么的,他能给我们的搬砖工作带来哪些好处呢? 举例说明:

如果类X包含一个指向某资源的指针,在左值语义下,类X的复制构造函数定义如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
X::X()
{
 // 申请资源(指针表示)
}
 
X::X(const X& other)
{
 // ...
 // 销毁资源
 // 克隆other中的资源
 // ...
}
 
X::~X()
{
 // 销毁资源
}

假设应用代码如下。其中,对象tmp被赋给a之后,便不再使用。

?
1
2
3
X tmp;
// ...经过一系列初始化...
X a = tmp;

在上面的代码中,执行步骤:

  • 先执行一次默认构造函数(默认构造tmp对象)
  • 再执行一次复制构造函数(复制构造a对象)
  • 退出作用域时执行析构函数(析构tmp和a对象)

从资源的视角来看,上述代码中共执行了2次资源申请和3次资源释放。

那么问题来了,既然对象tmp只是一个临时对象,在执行X a = tmp;时,对象a能否将tmp的资源'偷'过来,直接为我所用,而不影响原来的功能? 答案是可以。

?
1
2
3
4
X::X(const X& other)
{
 // 使用std::swap交换this和other的资源 
}

通过'偷'对象tmp的资源,减少了资源申请和释放的开销。而std::swap交换指针代价极小,可忽略不计。

3.3 How?

到现在为止,我们明白了std::move将要达到的效果,那么它究竟是怎么实现的呢?

?
1
2
3
4
5
6
7
template<class T>
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
 typedef typename remove_reference<T>::type&& RvalRef;
 return static_cast<RvalRef>(a);
}

不管输入参数为左值还是右值,都被remove_reference去掉其引用属性,RvalRef为右值类型,最终返回类型为右值引用。

3.4 Example

在实际使用中,一般将临时变量作为std::move的输入参数,并将返回值传入接受右值类型的函数中,方便其'偷取'临时变量中的资源。需要注意的是,临时变量被'偷'了之后,便不能对其进行读写,否则会产生未定义行为。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <utility>   
#include <iostream>  
#include <string>   
#include <vector>   
        
void foo(const std::string& n)
{       
 std::cout << "lvalue" << std::endl;
}       
        
void foo(std::string&& n) 
{       
 std::cout << "rvalue" << std::endl;
}       
        
void bar()     
{       
 foo("hello");    // rvalue
 std::string a = "world"
 foo(a);      // lvalue
 foo(std::move(a));   // rvalue
}
 
int main()
{
 std::vector<std::string> a = {"hello", "world"};
 std::vector<std::string> b;
 
 b.push_back("hello");   // 开销:string复制构造
 b.push_back(std::move(a[1])); // 开销:string移动构造(将临时变量a[1]中的指针偷过来)
 
 std::cout << "bsize: " << b.size() << std::endl;
 for (std::string& x: b)
 std::cout << x << std::endl;
 bar();
 return 0;
}

4 std::forward

4.1 What?

std::forward用于实现完美转发。那么什么是完美转发呢?完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。

简单来说,std::move用于将左值或右值对象强转成右值语义,而std::forward用于保持左值对象的左值语义和右值对象的右值语义。

4.2 Why?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <utility>
#include <iostream>
 
void bar(const int& x)
{
 std::cout << "lvalue" << std::endl;
}
 
void bar(int&& x)
{
 std::cout << "rvalue" << std::endl;
}
 
template <typename T>
void foo(T&& x)
{
 bar(x);
}
 
int main()
{
 int x = 10;
 foo(x); // 输出:lvalue
 foo(10); // 输出:lvalue
 return 0;
}

执行以上代码会发现,foo(x)和foo(10)都会输出lvalue。foo(x)输出lvalue可以理解,因为x是左值嘛,但是10是右值,为啥foo(10)也输出lvalue呢?

这是因为10只是作为函数foo的右值参数,但是在foo内部,10被带入了形参x,而x是一个有名字的变量,即右值,因此foo中bar(x)还是输出lvalue。

那么问题来了,如果我们想在foo函数内部保持x的右值语义,该怎么做呢?std::forward便派上了用场。

只需改写foo函数:

?
1
2
3
4
5
template <typename T>
void foo(T&& x)
{
 bar(std::forward<T>(x));
}

4.3 How?

std::forward听起来有点神奇,那么它到底是如何实现的呢?

?
1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T, typename Arg>
shared_ptr<T> factory(Arg&& arg)
{
 return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}
template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
 return static_cast<S&&>(a);
}
 
X x;
factory<A>(x);

如果factory的输入参数是一个左值,那么Arg = X&,根据叠加规则,std::forward<Arg> = X&。因此,在这种情况下,std::forward<Arg>(arg)仍然是左值。

相反,如果factory输入参数是一个右值,那么Arg = X,std::forward<Arg> = X。这种情况下,std::forward<Arg>(arg)是一个右值。

恰好达到了保留左值or右值语义的效果!

4.4 Example

直接上代码。如果前面都懂了,相信这段代码的输出结果也能猜个八九不离十了。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <utility>
#include <iostream>
 
void overloaded(const int& x)
{
 std::cout << "[lvalue]" << std::endl;
}
 
void overloaded(int&& x)
{
 std::cout << "[rvalue]" << std::endl;
}
 
template <class T> void fn(T&& x)
{
 overloaded(x);
 overloaded(std::forward<T>(x));
}
 
int main()
{
 int i = 10;
 overloaded(std::forward<int>(i));
 overloaded(std::forward<int&>(i));
 overloaded(std::forward<int&&>(i));
 
 fn(i);
 fn(std::move(i));
 
 return 0;
}

以上就是C++ 右值语义相关总结的详细内容,更多关于C++ 右值语义的资料请关注服务器之家其它相关文章!

原文链接:https://www.cnblogs.com/taiyang-li/p/14450396.html

延伸 · 阅读

精彩推荐
  • C/C++关于C语言中E-R图的详解

    关于C语言中E-R图的详解

    今天小编就为大家分享一篇关于关于C语言中E-R图的详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看...

    Struggler095962021-07-12
  • C/C++C语言main函数的三种形式实例详解

    C语言main函数的三种形式实例详解

    这篇文章主要介绍了 C语言main函数的三种形式实例详解的相关资料,需要的朋友可以参考下...

    ieearth6912021-05-16
  • C/C++C语言实现双人五子棋游戏

    C语言实现双人五子棋游戏

    这篇文章主要为大家详细介绍了C语言实现双人五子棋游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    两片空白7312021-11-12
  • C/C++c/c++内存分配大小实例讲解

    c/c++内存分配大小实例讲解

    在本篇文章里小编给大家整理了一篇关于c/c++内存分配大小实例讲解内容,有需要的朋友们可以跟着学习参考下。...

    jihite5172022-02-22
  • C/C++c/c++实现获取域名的IP地址

    c/c++实现获取域名的IP地址

    本文给大家汇总介绍了使用c/c++实现获取域名的IP地址的几种方法以及这些方法的核心函数gethostbyname的详细用法,非常的实用,有需要的小伙伴可以参考下...

    C++教程网10262021-03-16
  • C/C++深入C++拷贝构造函数的总结详解

    深入C++拷贝构造函数的总结详解

    本篇文章是对C++中拷贝构造函数进行了总结与介绍。需要的朋友参考下...

    C++教程网5182020-11-30
  • C/C++使用C++制作简单的web服务器(续)

    使用C++制作简单的web服务器(续)

    本文承接上文《使用C++制作简单的web服务器》,把web服务器做的功能稍微强大些,主要增加的功能是从文件中读取网页并返回给客户端,而不是把网页代码...

    C++教程网5492021-02-22
  • C/C++OpenCV实现拼接图像的简单方法

    OpenCV实现拼接图像的简单方法

    这篇文章主要为大家详细介绍了OpenCV实现拼接图像的简单方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...

    iteye_183805102021-07-29