现代C++之右值语义

简介: 现代C++之右值语义
  • 1 什么是左值,什么是右值?


  • 2 引用叠加规则


  • 3 std::move


  • 3.1 What?


  • 3.2 Why?


  • 3.3 How?


  • 3.4 Example


  • 4 std::forward


  • 4.1 What?


  • 4.2 Why?


  • 4.3 How?


  • 4.4 Example




现代C++之右值语义


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



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


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


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


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




2 引用叠加规则


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


A& + A& = A&
A& + A&& = A&
A&& + A& = A&
A&& + A&& = A&&


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


  • 如果Tint&类型, T&&int&x为左值语义


  • 如果Tint&&类型, T&&int&&, x为右值语义


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




3 std::move


3.1 What?


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


3.2 Why?


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

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


X::X() 
{
  // 申请资源(指针表示)
}
X::X(const X& other)
{
  // ...
  // 销毁资源 
  // 克隆other中的资源
  // ...
}
X::~X() 
{
  // 销毁资源
}


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


X tmp;
// ...经过一系列初始化...
X a = tmp;


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


  • 先执行一次默认构造函数(默认构造tmp对象)


  • 再执行一次复制构造函数(复制构造a对象)


  • 退出作用域时执行析构函数(析构tmp和a对象)


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


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


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


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



3.3 How?


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


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的输入参数,并将返回值传入接受右值类型的函数中,方便其'偷取'临时变量中的资源。需要注意的是,临时变量被'偷'了之后,便不能对其进行读写,否则会产生未定义行为。


#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?


#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)都会输出lvaluefoo(x)输出lvalue可以理解,因为x是左值嘛,但是10是右值,为啥foo(10)也输出lvalue呢?


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


那么问题来了,如果我们想在foo函数内部保持x的右值语义,该怎么做呢?

std::forward便派上了用场。

只需改写foo函数:

template <typename T>
void foo(T&& x)
{
  bar(std::forward<T>(x));
}



4.3 How?



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


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 = Xstd::forward<Arg> = X。这种情况下,std::forward<Arg>(arg)是一个右值。

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



4.4 Example



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

#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;
}
相关文章
|
6月前
|
算法 编译器 程序员
【C/C++ 解惑 】 std::move 将左值转换为右值的背后发生了什么?
【C/C++ 解惑 】 std::move 将左值转换为右值的背后发生了什么?
64 0
|
2月前
|
编译器 C++
C++ 11新特性之右值引用
C++ 11新特性之右值引用
40 1
|
6月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
35 1
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(中)
|
6月前
|
存储 安全 C语言
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(上)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
31 2
|
6月前
|
编译器 C语言 C++
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值(下)
从C语言到C++_33(C++11_上)initializer_list+右值引用+完美转发+移动构造/赋值
34 1
|
5月前
|
编译器 C++ 开发者
C++一分钟之-右值引用与完美转发
【6月更文挑战第25天】C++11引入的右值引用和完美转发增强了资源管理和模板灵活性。右值引用(`&&`)用于绑定临时对象,支持移动语义,减少拷贝。移动构造和赋值允许有效“窃取”资源。完美转发通过`std::forward`保持参数原样传递,适用于通用模板。常见问题包括误解右值引用只能绑定临时对象,误用`std::forward`,忽视`noexcept`和过度使用`std::move`。高效技巧涉及利用右值引用优化容器操作,使用完美转发构造函数和创建通用工厂函数。掌握这些特性能提升代码效率和泛型编程能力。
45 0
|
6月前
|
编译器 C++ 容器
【C++11(一)】右值引用以及列表初始化
【C++11(一)】右值引用以及列表初始化
|
6月前
|
存储 安全 程序员
C++11:右值引用
C++11:右值引用
37 0
|
6月前
|
存储 算法 程序员
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
【C++入门到精通】右值引用 | 完美转发 C++11 [ C++入门 ]
50 0
|
6月前
|
存储 人工智能 编译器
【重学C++】【引用】一文看懂引用的本质与右值引用存在的意义
【重学C++】【引用】一文看懂引用的本质与右值引用存在的意义
131 0