前言
移动语义可以被认为是C++11的首要特性。它不仅允许编译器用相对便宜的移动替换昂贵的拷贝操作,并且在实际中要求编译器如此运转(在条件满足时),但本节要叙述的是:不要过高地看待移动语义。
类型支持
首先,并非所有类型均支持移动语义。在实际使用过程中,可能你当前使用的类型(历史遗留代码)并不支持移动语义。确实,C++11愿意为缺少它们的类生成move操作,但这只发生在声明没有copy操作,移动操作或析构函数的类中(见Item17)。此外,不支持move的data member或base class)也将抑制编译器生成move操作。
支持也未必高效
其次,即使该类型明确具备移动语义,它也未必如你所想般高效。举例而言,C++标准库中的所有容器均支持移动语义,但对于某些容器来说,它们的移动语义并不具备真正的低开销,又或者由于容器元素类型的限制,它们无法采用真正高效的move操作。
std::array
以std::array为例,该容器是C++11中的一个新容器。std::array本质上是一个带有STL接口的内置数组。一般而言,STL容器倾向于将其所有元素均保存在堆上,内部元素仅仅是一个指向堆中内容的指针(真实情况较为复杂,但其差异不影响本次讨)。这些指针的存在为移动容器提供了便利:只需要将现有指针指向对应内容,然后将指针置为nullptr即可,现以移动vector为例:1
2
3
4
5std::vector<Widget> vw1;
// put data into vw1
…
// move vw1 into vw2. Runs in constant time. Only ptrs in vw1 and vw2 are modified
auto vw2 = std::move(vw1);
但std::array将元素直接存储于对象内部,因此其move操作看起来如下所示:
1 | std::array<Widget, 10000> aw1; |
这里需要注意的是,我们必须移动aw1中每一个Widget至aw2,也就是说当前仍然需要线性的时间,可能这快于copy,但也绝非像某些人所说的“移动容器只需要执行拷贝一个指针的时间”。
SSO std::string
又例如,std::string提供常数时间的移动操作与线性时间的拷贝操作,这让我们感觉移动必然快于拷贝,但情况可能并非如此。许多字符串实现采用小字符串优化(SSO)。对于SSO,“小”字符串(例如,容量不超过15个字符的字符串)存储于std::string对象内的buffer中,而并非使用堆中的内存。移动基于SSO技术实现的字符串并不快于拷贝,因为传统仅拷贝一个指针的行为对其并不适用(内容存储于对象内,因此必须对对象有所操作),对于此类对象,copy并不慢于move。
异常安全性
即使一切安好,move操作最终也可能会导致copy操作。在Item14中曾经提及,为了不破坏历史遗留代码,C++11中的异常规格必然与C++98中相容,因此,对于某些容器,只有在move操作明确不会抛出异常时才可能会取代copy操作。因此,即使move操作确实优于copy操作且当前环境适用于move(例如参数为右值),编译器在move操作并未声明为noexcept的情况下依然会选择对move视而不见。
总结
- 在撰写代码时务必谨慎假设move语义并未发挥作用,但如果你明确当前类型必然支持移动语义,那自然可以大胆使用。