C++移动语义浅析

简介

作为 C++11 支持的新特性,移动语义允许我们以一种更轻量级的(相较于拷贝)形式实现对象资源的复用。这里需要注意的是,移动语义移动的是对象所持有的资源,而不是对象本身。

C++ 值的类型

在 C++ 中值的类型可以被分为三种:

  • 左值:has identity,but can’t be moved from。包括:
    • 变量、函数或者数据成员的名字;
    • 返回左值引用的表达式,比如 ++x、x = 1;
    • 字符串字面量,如 “hello world”
  • 纯右值:no identity,but can be moved from。包括:
    • 返回非引用类型的表达式,比如 x++、x + 1;
    • 除字符串字面量之外的字面量,比如 42、true;
    • this 指针也是纯右值
  • 将亡值:has identity, can be moved from。包括:
    • 右值引用类型的返回值,比如 std::move(x)

右值引用

为了支持移动语义,C++11 引入了右值引用类型,因此 C++ 中就有了三种引用类型:

  • 右值引用:只能绑定到右值上,比如 int &&;
  • 非 const 的左值引用: 只能绑定到左值上,比如 int &;
  • const 的左值引用: 可以绑定到左值或右值上,比如 const int &

移动构造和移动赋值成员函数

如何定义一个移动构造或移动赋值成员函数

作为一个移动构造或移动赋值成员函数,要满足以下要求:

  • 完成资源移动。资源的所有权移交给新创建的对象或赋值对象;
  • 确保移动操作完成后,销毁源对象是无害的。也就是说源对象不再指向被移动的资源,比如成员指针被设置为nullptr;
  • 确保移动操作完成后,源对象依然是有效的。也就是说,源对象可以被赋予一个新值,再次持有新的资源。
  • 声明为 noexcept, 这是因为标准库中的一些容器接口需要作出不会抛出异常的保证。比如说vector中的push_back接口:
If an exception is thrown (which can be due to Allocator::allocate() or element copy/move constructor/assignment), this function has no effect (strong exception guarantee).

这里给出一个移动赋值成员函数的通用实现。它采用拷贝并交换, 它会根据右侧对象是左值还是右值,调用拷贝构造或者移动构造去初始化形参rhs,从而同时实现拷贝赋值和移动赋值语义。

ClassA& ClassA::operator=(ClassA rhs)
{
    swap(*this, rhs);
    return *this;
}

移动和拷贝成员函数的匹配原则

考虑下面示例:

Widgt wt1;
Widgt wt2 = std::move(wt1);

如果定义了移动构造函数,那么编译器会优先选择移动构造函数。如果没有定义移动构造函数,参数为const的左值引用的拷贝构造函数就会被匹配。

移动赋值亦是如此。

何时定义移动构造/赋值

这里先列出编译器合成这些函数的规则:

  • 规则1: 如果某个类有自定义拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符了。根据函数匹配规则,这种情况下会调用拷贝操作来处理右值
  • 规则2: 如果某个类定义了移动构造函数,没有定义拷贝构造函数,那么后者被编译器定义为删除的(对于赋值运算符也是一样的)。

一般情况下,建议如果定义了其中的一个,最好都定义了,理由如下:

  • 如果你析构函数里要做事,不管是释放内存资源、锁还是关闭数据库连接,那么你就应该把析构函数的这些好兄弟都定义出来;
  • 如果定义了这些操作中的某一个,就应该把其他的操作都定义出来,以避免所有(潜在的)可移动的场景都变成昂贵的拷贝(对应规则 1)或者使得类型变成仅能移动的(对应规则 2)。

std::move和std::forward

首先,简要说明一下这两个函数的作用:

  • std::move 无条件地将实参转换为右值;
  • std::forward 将左值、左值引用转换为左值,将右值、右值引用转换为右值

std::move 的实现

template <typename T> typename remove_reference<T>::type &&move(T &&param) {
  using ReturnType = typename remove_reference<T>::type &&;

  return static_cast<ReturnType>(param);
}

这里 T&& 是通用引用,因此这个函数几乎可以接收任何类型的参数。

通过 remove_reference 去掉 T 的引用性质(并不会去掉 cv 限定符,即const和volatile),然后给它加上 &&,形成 ReturnType 类型,由于右值引用类型的返回值是右值,因此结果是实参被无条件地转换为右值。

所以,需要注意:使用 std::move 并不代表移动操作一定会发生。具体原因如下:

  • 可能这个类型根本没有定义移动操作;
  • std::move 并不会去除实参的 const 性质,因此把 const 的对象传给它,得到的返回值类型也是 const 的,对它的操作会变为拷贝操作

std::forward 和完美转发

先看std::forward的实现:

template< class T >
T&& forward( typename std::remove_reference<T>::type& t ) noexcept {  
    return static_cast<T&&>(param);  
}

template< class T >
T&& forward( typename std::remove_reference<T>::type&& t ) noexcept {  
    return static_cast<T&&>(param);
}

它可以将一个左值、左值引用转换为左值,将一个右值、右值引用转换为右值。

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数,转发后需要保持被转发实参的所有性质,包括

  • 实参是否是 const 的;
  • 实参是左值还是右值

这种场景我们往往称之为完美转发,C++11 可以通过 std::forward 来实现。比如make_unique的实现代码:

template <typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts &&... params) {
  return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

移动语义和返回值优化

匿名返回值优化

如果 return 语句的表达式是纯右值,且它和返回值的类型相同。此时,编译器可以实施 copy elision(拷贝省略、拷贝消除),将对象直接构造到调用者的栈上去。具体示例如下:

T f() {
    return T();
}
 
f(); // only one call to default constructor of T

命名返回值优化NRVO

先看如下代码:

X bar()  
{  
   X xx;  
   // ...  
   return xx;  
}

对于上面的函数 bar,编译器会直接用参数 __result 代替命名的返回值 xx,即改写为:

void  
bar( X &__result )  
{  
 
   __result.X::X();  
 
   // ...  
   return;  
}

也就是说返回值会被直接构造在调用者的栈上,少了一次拷贝操作,这种优化被称为 Named Return Value Optimization(NRVO)。

移动语义和NRVO

C++11 开始,NRVO 仍可以发生,但在没有 NRVO 的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。

这一移动行为不需要程序员手工用 std::move 进行干预,使用 std::move 对于移动行为没有帮助,反而会影响返回值优化,因为相对于 std::move,返回值优化可以减少一次移动构造函数的调用。

参考

[1] C++ Primer, 第五版
[2] C++ Core Guidelines
[3] 移动语义和完美转发浅析
[4] 在纷繁多变的世界里茁壮成长:C++ 2006–2020

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值