Noexcept和copy,移动构造函数

时间:2015-02-20 10:58:37

标签: c++ c++11 noexcept

无处不在我看起来似乎是协议,当移动构造函数为noexcept(false)时,标准库必须调用复制构造函数而不是移动构造函数。

现在我不明白为什么会这样。而且Visual Studio VC v140和gcc v 4.9.2似乎也有不同的做法。

我不明白为什么不这样做,除非这是例如向量。我的意思是如果T没有,vector :: resize()应该如何能够提供强大的异常保证。正如我所看到的那样,矢量的异常级别将取决于T.无论是否使用复制或移动。 我理解no除了仅仅是对编译器进行异常处理优化的眨眼。

这个小程序在使用gcc编译时调用复制构造函数,并在使用Visual Studio编译时移动构造函数。

include <iostream>
#include <vector>

struct foo {
  foo() {}
  //    foo( const foo & ) noexcept { std::cout << "copy\n"; }
  //    foo( foo && ) noexcept { std::cout << "move\n"; }
  foo( const foo & )  { std::cout << "copy\n"; }
  foo( foo && )  { std::cout << "move\n"; }

  ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

2 个答案:

答案 0 :(得分:22)

这是一个多方面的问题,所以在我经历各个方面的时候请耐心等待。

标准库期望所有用户类型始终提供基本的异常保证。这种保证说,当抛出异常时,所涉及的对象仍处于有效(如果未知)状态,没有资源泄露,没有违反基本语言不变量,并且没有发生远距离的怪异动作(最后一次)一个不是正式定义的一部分,但它实际上是一个隐含的假设。)

考虑类Foo的复制构造函数:

Foo(const Foo& o);

如果此构造函数抛出,基本异常保证将为您提供以下知识:

  • 未创建新对象。如果构造函数抛出,则不会创建该对象。
  • o未被修改。它唯一的参与是通过const引用,所以不能修改它。其他案件属于远距离&#34;幽灵般的行动。标题,或可能&#34;基本语言不变&#34;。
  • 没有资源被泄露,整个计划仍然是连贯的。

在移动构造函数中:

Foo(Foo&& o);

基本保证给予较少的保证。可以修改o,因为它通过非const引用涉及,因此它可能处于任何状态。

接下来,请查看vector::resize。它的实施通常遵循相同的方案:

void vector<T, A>::resize(std::size_t newSize) {
  if (newSize == size()) return;
  if (newSize < size()) makeSmaller(newSize);
  else if (newSize <= capacity()) makeBiggerSimple(newSize);
  else makeBiggerComplicated(newSize);
}
void vector<T, A>::makeBiggerComplicated(std::size_t newSize) {
  auto newMemory = allocateNewMemory(newSize);
  constructAdditionalElements(newMemory, size(), newSize);
  transferExistingElements(newMemory);
  replaceInternalBuffer(newMemory, newSize);
}

这里的关键功能是transferExistingElements。如果我们只使用复制,它有一个简单的保证:它不能修改源缓冲区。因此,如果操作抛出的任何时刻,我们可以破坏新创建的对象(请记住标准库绝对不能用于抛出析构函数),抛弃新缓冲区,然后重新抛出。矢量看起来好像从未被修改过。这意味着我们有强有力的保证,即使元素的复制构造函数只提供弱保证。

但如果我们使用移动,那么这不起作用。移动一个对象后,任何后续异常都意味着源缓冲区已更改。而且因为我们无法保证移动物体也不会抛出,我们甚至无法恢复。因此,为了保持强有力的保证,我们必须要求移动操作不会抛出任何异常。如果我们有,那我们就没事了。这就是为什么我们有move_if_noexcept

关于MSVC和GCC之间的区别:自版本14以来,MSVC仅支持noexcept,并且由于仍在开发中,我怀疑标准库尚未更新以利用。< / p>

答案 1 :(得分:11)

核心问题是,使用投掷移动构造函数提供强大的异常安全是不可能的。想象一下,如果在向量调整大小时,将元素移动到新缓冲区的中途,移动构造函数会抛出。你怎么可能恢复以前的状态?你不能再使用移动构造函数,因为,这可能只是继续投掷。

复制适用于强大的异常安全保证,无论它是否因为原始状态没有损坏而抛出性质,因此如果您无法构建整个新状态,则可以清理部分构建状态,然后你就完成了,因为旧的状态仍在等着你。移动施工人员不提供此安全网。

从根本上不可能提供强烈的异常安全resize()与投掷移动,但 easy 与投掷副本。这一基本事实反映在标准库的各处。

GCC和VS对此有不同的看法,因为它们处于不同的一致性阶段。 VS已经让noexcept成为他们实现的最后一个功能之一,因此他们的行为是C ++ 03行为与C ++ 11/14之间的一种中间行为。特别是,由于他们无法判断您的移动构造函数是否实际为noexcept,因此他们基本上只需要猜测。从内存中他们只是假设它是noexcept,因为抛出移动构造函数并不常见且无法移动将是一个关键问题。