C++11移动构造函数未被调用,首选默认构造函数。

26

假设我们有这个类:

class X {
public:
    explicit X (char* c) { cout<<"ctor"<<endl; init(c); };
    X (X& lv)  { cout<<"copy"<<endl;  init(lv.c_); };
    X (X&& rv) { cout<<"move"<<endl;  c_ = rv.c_; rv.c_ = nullptr; };

    const char* c() { return c_; };

private:
    void init(char *c) { c_ = new char[strlen(c)+1]; strcpy(c_, c); };
    char* c_;

};

并且这是一个示例用法:

X x("test");
cout << x.c() << endl;
X y(x);
cout << y.c() << endl;
X z( X("test") );
cout << z.c() << endl;

输出结果为:

ctor
test
copy
test
ctor   <-- why not move?
test

我正在使用默认设置的VS2010。我期望最后一个对象(z)应该被移动构造,但实际上它并没有被移动构造! 如果我使用X z( move(X("test")) );,那么输出的最后几行将是ctor move test,这正是我期望的。这是(N)RVO的情况吗?

Q: 根据标准,移动构造函数应该被调用吗?如果是,为什么没有被调用?


2
这是复制省略。如果复制省略失败,那么就会发生移动。为什么你的帖子标题说“默认构造函数优先”?没有调用默认构造函数,也没有任何东西取代移动构造函数。它被完全消除了。 - Benjamin Lindley
这段代码应该无法编译,因为自C++11以来,字符串字面值不能隐式转换为非const的char * - M.M
可能是什么是复制省略和返回值优化?的重复问题。 - underscore_d
如果你使用 g++ 编译器,那么请传递标志 -fno-elide-constructors,这将关闭复制省略并调用移动构造函数。 - Nilesh Kumar
4个回答

32
您所看到的是复制省略,它允许编译器直接将临时对象构造到目标对象中并省略复制(或移动)构造/析构函数。编译器允许应用复制省略的情况在C++11标准的§12.8.32中规定:
当满足特定条件时,一个实现允许省略类对象的复制/移动构造函数,即使该对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下,实现将省略的复制/移动操作源和目标视为仅是引用同一对象的两种不同方式,并且该对象的销毁发生在没有优化的情况下两个对象将被销毁的时间的较晚者。这种省略复制/移动操作的技术称为复制省略,可以在以下情况下使用(可以组合以消除多个复制):
  • 在返回类型为类类型的函数中的return语句中,当表达式是非易失性自动对象的名称,并且具有与函数返回类型相同的cv-unqualified类型时,通过直接将自动对象构造到函数的返回值中,可以省略复制/移动操作
  • 在throw表达式中,当操作数是非易失性自动对象的名称,其作用域不延伸到最内层尝试块的结束时(如果有),则可以通过直接将自动对象构造到异常对象中省略从操作数到异常对象(15.1)的复制/移动操作
  • 当一个未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-unqualified类型的类对象时,可以通过直接将临时对象构造到省略复制/移动的目标中来省略复制/移动操作
  • 当异常处理程序(第15条款)的异常声明(除了cv-qualification之外)声明与异常对象(15.1)具有相同类型的对象时,如果程序的含义除执行由异常声明声明的对象的构造函数和析构函数之外没有改变,则可以通过将异常声明视为异常对象的别名来省略复制/移动操作。

1
你能提供一个简单的示例,不使用 std::move 强制编译器使用移动构造函数吗? - emesx
2
@elmes: X z((rand() % 2) ? X("测试") : X("测试")); - Benjamin Lindley
@elmes:请记住,尽管标准允许进行复制省略,但它并不强制执行。因此,在更复杂的情况下,不能保证您实际上会获得复制省略。典型的例子是由Benjamin Lindley提到的代码,其中你有一个函数/表达式,根据运行时参数可以返回不同的对象。此外,即使只在显式移动对象时才调用对象的移动构造函数,移动构造函数也是有用的,因为标准容器/算法将在可能的情况下使用它(如果移动构造函数是noexcept)。 - Grizzly
1
@Grizzly:C++11 要求在可以进行复制省略(即从函数返回相同类型对象)时,每个 return local_var; 都会首先尝试移动 local_var。不需要显式移动,这会妨碍 (N)RVO。 - Xeo
2
@Xeo:我不太明白。标准可能即使在描述的情况下也允许复制省略,但编译器是否能够执行,特别是如果生命周期重叠,这是有问题的。而我何曾说过在返回本地变量时需要(或者希望)显式移动呢? - Grizzly
显示剩余2条评论

3
你在第三行代码中得到的ctor输出是用于构建临时对象的。在此之后,临时对象被移动到新变量z中。在这种情况下,编译器可以选择省略复制/移动操作,似乎这就是它所做的。
标准规定:
(§12.8/31)当满足某些条件时,实现允许省略类对象的复制/移动构造,即使该对象的复制/移动构造函数和/或析构函数具有副作用。[...]这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以组合以消除多个副本):
[...]
- 当一个未绑定到引用(12.2)的临时类对象将被复制/移动到具有相同cv-unqualified类型的类对象时,可以通过直接将临时对象构造到省略的复制/移动的目标中来省略复制/移动操作
[...]
其中一个重要条件是源对象和目标对象的类型相同(除了cv限定符,例如const)。
因此,你可以强制调用移动构造函数的一种方法是将对象初始化与隐式类型转换结合使用:
#include <iostream>

struct B
{};

struct A
{
  A() {}
  A(A&& a) {
    std::cout << "move" << std::endl;
  }
  A(B&& b) {
    std::cout << "move from B" << std::endl;
  }
};


int main()
{
  A a1 = A(); // move elided
  A a2 = B(); // move not elided because of type conversion
  return 0;
}

1
A::A(B&& b) 可以被视为移动构造函数吗?N3936草案(12.8.3)指出:“如果类X的非模板构造函数的第一个参数是类型为X&&、const X&&、volatile X&&或const volatile X&&,并且要么没有其他参数,要么所有其他参数都有默认参数,则该构造函数是移动构造函数。” - Bojan Komazec

0

你正在显式调用 Xchar* 构造函数 X("test")

因此它会打印出 ctor


1
当声明 z 时,调用 X 的移动构造函数。 - Xeo

0
只想评论一下,如果你只想确保移动构造函数正常工作,你可以通过添加条件来修改代码以消除编译器优化,例如:
X z( some_val > 1 ? X("test") : X("other test"));

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