#本周小贴士#77:临时的,移动的,和拷贝的

Posted -飞鹤-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#本周小贴士#77:临时的,移动的,和拷贝的相关的知识,希望对你有一定的参考价值。

作为totw/77最初发表于2014年7月9日

由Titus Winters (titus@google.com)创作

更新于2017年10月20日

在不断尝试着给对编程语言不熟悉的人解释C++11是如何改变事物的过程中时,我们展示了“拷贝什么时候产生?”的系列中的另一个条目。这是简化C++中有关拷贝微妙规则并用一系列更简单的规则来替换的一般尝试中的一部分。

你能计数到2吗?

你能?太好了。请记住,“命名规则”意味着你可以为某个特定的资源分配一个唯一的名字,这会影响在流通中对象的副本数。(请参阅TotW55关于命名计数)

简而言之,命名计数

如果你担心一个拷贝被创建,你大概特别担心代码的某些行。因此,看到那个点。你认为正拷贝的数据有多少个名字存在呢?这里只有3种情况需要考虑:

二个名字:它是一个副本

这很简单:如果你给一个相同的数据第二个名字,那么它就是一个副本。

std::vector<int> foo;
FillAVectorOfIntsByOutputParameterSoNobodyThinksAboutCopies(&foo);
std::vector<int> bar = foo;     // 是的,这是一个副本

std::map<int, string> my_map;
string forty_two = "42";
my_map[5] = forty_two;          // 同样是一个副本:my_map[5]作为一个名字

一个名字,它是一个移动对象

这有点令人惊讶:C++11识别到如果你不再引用一个名字,那么你也就不再需要关心那个数据了。该语言必须小心不要破坏你依赖的析构函数(例如absl::MutexLock),因此return是容易识别的情况。

std::vector<int> GetSomeInts() {
  std::vector<int> ret = {1, 2, 3, 4};
  return ret;
}

// 仅仅是一个移动,"ret"或"foo"有数据,但是不是同时拥有
std::vector<int> foo = GetSomeInts();

另外一种方法是告诉编译器你已经处理了名字(来自TotW55“名称擦除器”),这种方法是调用std::move().

std::vector<int> foo = GetSomeInts();
// 不是一个拷贝,移动允许编译器将foo作为一个临时量,因此
// 这正在调用std::vector<int>的移动构造函数
// 注意,它不是通过std::move来完成移动,而是通过构造函数。
// 调用std::Move仅允许将foo作为一个临时对象而不是一个有名对象)
std::vector<int> bar = std::move(foo);

没有名字:它是一个临时对象

临时对象也是特殊的:如果你想避免副本,那么请避免为变量提供名字

void OperatesOnVector(const std::vector<int>& v);

// 没有副本:在由GetSomeInts()返回的vector中值将被移动(O(1))到
// 由这些调用之间构造的临时对象中,并且通过引用传递到OperateOnVector()
OperatesOnVector(GetSomeInts());

注意:僵尸

以上(除了std::move本身)的是希望非常直观,只是我们在C++11之前建立了奇怪的副本概念。对于没有垃圾回收的语言而言,这种解释的类型给我们提供了极好的性能和清晰度上的组合。然而,它不是没有危险,其中最大的是:一个值被移动之后,剩下了什么?

T bar = std::move(foo);
CHECK(foo.empty()); // 这是有效的吗?可能,但不应该依赖它

这是主要的困难之一:我们可以对这些剩下来的值说什么?对于大多数标准库类型而言,这样的值处于“有效但未指定的状态”。非标准类型通常也遵循相同的规则。安全的方法是远离这些对象:你可以对它们重新赋值,或者让它们离开作用域,但是不要对它们的状态做出任何其他的假设。

Clang-tidy提供了一些静态检测,通过misc-use-after-move检测来捕获移动后的使用。然而,静态分析并不会捕获所有这些担心的情况。在代码审查中指出这些,并在你的代码中避免它们。远离这些僵尸。

等等,std::move没有移动?

是的,另一件要注意的事情是,调用std::move实际上并不移动它自己,它仅是一个右值引用转换。只有使用移动构造函数或移动赋值函数才能完成任务。

std::vector<int> foo = GetSomeInts();
std::move(foo); // 不做任何事情
// 调用std::vector<int>的移动构造函数
std::vector<int> bar = std::move(foo);

这几乎不会发生,你可能不应该浪费精力在它上面。如果在std::move和移动构造函数之间的连接让你困惑,其实我真的只是提到它而已。

啊!这一切很复杂呀!为什么呢!?!

首先:它真的没有这样的槽。针对大多数我们自己的值类型,我们有移动操作,所以我们可以废除所有关于“它是一个副本吗?这样有效吗”的讨论,而仅依靠名称计数:两个名字,一个副本。少于那个:没有副本。

忽略副本的问题,用值语义推导更清晰更简单。考虑这两种操作:

void Foo(std::vector<string>* paths) {
  ExpandGlob(GenerateGlob(), paths);
}

std::vector<string> Bar() {
  std::vector<string> paths;
  ExpandGlob(GenerateGlob(), &paths);
  return paths;
}

这些是相同的吗?如果*paths中存在数据怎么办呢?你怎么知道?对于读者来说,值语义比输入/输出更容易推导,此刻你需要考虑(并记录)现有数据发生了什么,以及是否有可能存在指针所有权转移。

在处理值(而不是指针)时,由于存在对生命周期和用法更简单的保证,编译器的优化器更容易对这种风格的代码进行操作。管理良好的值语义也能最小化触发分配器(这是代价小的但不是免费的)。一旦我们理解了移动语义如何帮助我们摆脱副本,编译器的优化器就可以更好地推导对象类型,生命周期,虚分派和许多其他有助于产生更高效机器码的问题。

由于现在大多数的实用程序代码是有考虑移动语义的,因此我们应该停止担心副本和指针语义,而专注于编写易于理解地代码。请确保你了解这些新规则:并非你遇到的所有老接口都能够更新为按值(代替按输出参数)返回,所以混合的形式将一直存在。对你而言,理解什么时候一种情况比另一种情况更适合是很重要的。

以上是关于#本周小贴士#77:临时的,移动的,和拷贝的的主要内容,如果未能解决你的问题,请参考以下文章

本周小贴士#116: 拷贝消除与值传递

本周小贴士#116: 拷贝消除与值传递

本周小贴士#143:C++11 删除的函数(= delete)

本周小贴士#122: 测试固定装置,清晰度和数据流

本周小贴士#74:委托和继承构造函数

本周小贴士#101:返回值,引用和生命周期