vector::push_back坚持使用拷贝构造函数,尽管已经提供了移动构造函数。

19
我从gcc收到一个奇怪的错误,但无法弄清楚原因。我编写了以下示例代码,以便更清楚地说明问题。基本上,我定义了一个类,并将其复制构造函数和复制赋值运算符设为私有,以防止意外调用它们。
#include <vector>
#include <cstdio>
using std::vector;

class branch 
{
public:
  int th;

private:
  branch( const branch& other );
  const branch& operator=( const branch& other );

public:

  branch() : th(0) {}

  branch( branch&& other )
  {
    printf( "called! other.th=%d\n", other.th );
  }

  const branch& operator=( branch&& other )
  {
    printf( "called! other.th=%d\n", other.th );
    return (*this);
  }

};



int main()
{
  vector<branch> v;
  branch a;
  v.push_back( std::move(a) );

  return 0;
}

我期望这段代码能够编译通过,但是使用gcc编译时却失败了。实际上,gcc报错说"branch::branch(const branch&) is private",而根据我的理解,这个函数不应该被调用。
赋值运算符是正常工作的,因为如果我将main()函数的内容替换为
branch a;
branch b;
b = a;

它将按预期进行编译和运行。

这是gcc的正确行为吗?如果是,那么上述代码有什么问题? 对我来说,任何建议都是有帮助的。谢谢!


我之前使用的是gcc 4.7.1-2。我会尝试用4.6.1版本。谢谢! - BreakDS
2
根据我对N3242的阅读,这段代码应该是允许的(但如果移动构造函数抛出异常,则程序具有未定义行为)。 - aschepler
我尝试使用不同版本的gcc进行编译。确认它可以在gcc 4.6.3和4.6.1上工作。 - BreakDS
1
GCC在旧版本中没有检查这一点,但是从4.7开始,在这方面完全符合标准,这就是为什么你遇到了这个“问题”的原因。 - Lightness Races in Orbit
2个回答

18
尝试在移动构造函数声明中添加 "noexcept"。
我无法引用标准,但最近版本的gcc似乎要求复制构造函数为public或者移动构造函数被声明为 "noexcept"。无论是否有 "noexcept" 限定符,如果将复制构造函数设置为public,则它在运行时将表现出您期望的行为。

3
如果他将复制构造函数设置为public,则对象将被复制,这显然是他试图避免的。无论如何,将移动构造函数设置为noexcept是正确的解决方案,因此加一。 - ildjarn
谢谢你们两个,noexcept和public复制构造函数都是正确的。然而,有点违反直觉的是,复制构造函数不能被设为私有。 - BreakDS
@BreakDS:如果移动构造函数是noexcept的,那么复制构造函数可以被设为私有(假设标准库实现正确)。 - ildjarn
是的,我看到如果复制构造函数是私有的,移动构造函数是公共的,可能会出现问题。对于像我这样不熟悉noexcept关键字的人,可以看一下这个:使用noexcept和这个异常移动 - BreakDS

11

与之前的答案所建议的不同,gcc 4.7 拒绝了这段代码是 错误的 ,这个错误已经在gcc 4.8中得到了纠正

vector<T>::push_back 的完全符合标准的行为是:

如果只有一个拷贝构造函数而没有移动构造函数,push_back会复制其参数并提供强异常安全保障。也就是说,如果由于向量存储的重新分配引发的异常导致push_back失败,则原始向量将保持不变且可用。这是自C++98以来已知的行为,也是随之而来混乱的原因。
如果对于T存在noexcept移动构造函数,则push_back将从其参数中移动,并提供强异常安全保障。这里没有什么意外。
如果存在一个非noexcept移动构造函数和一个拷贝构造函数,push_back复制该对象,并提供强异常安全保障。这一点乍一看可能出人意料。虽然在此处push_back可以进行移动,但那只能以牺牲强异常保障为代价。如果您将代码从C++98移植到C++11且您的类型是可移动的,则会悄悄地更改现有的push_back调用的行为。为了避免这种陷阱并与C++98代码保持兼容性,C++11退回到较慢的复制。这就是gcc 4.7行为的全部内容。但还有更多...
如果存在一个非noexcept移动构造函数,但根本没有拷贝构造函数-也就是说,只能移动而不能复制元素-push_back将执行移动,但不会提供强异常安全保障。这就是gcc 4.7出了问题的地方。在C++98中,对于可移动但不可复制的类型没有push_back。因此,在这里牺牲强异常安全并不会破坏现有代码。这就是允许的原因,而原始代码实际上是合法的C++11代码。

请参考cppreference.compush_back函数:

如果抛出异常,该函数不会产生影响(强异常保证)。

如果T的移动构造函数不是noexcept且复制构造函数不可访问,则vector将使用抛出移动构造函数。如果它抛出异常,则保证被放弃并且效果未指定。

或者更加复杂的C++11标准§23.3.6.5(由我强调):

如果新大小大于旧容量,则导致重新分配。如果没有重新分配,则插入点之前的所有迭代器和引用仍然有效。如果除了T的复制构造函数、移动构造函数、赋值运算符或移动赋值运算符或任何InputIterator操作之外抛出异常,则没有效果。 如果非CopyInsertable T的移动构造函数抛出异常,则效果未指定。

如果您不喜欢阅读,可以观看Scott Meyer的Going Native 2013演讲(从0:30:20开始,有趣的部分大约在0:42:00左右)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接