在没有移动构造函数或移动赋值的情况下调用 std::move

Calling std::move without move constructor or move assignment

我们知道 std::move does not actually move anything。它只是将左值引用 (&) 转换为右值引用 (&&)。

那么在下面的例子中,拷贝构造函数是怎么调用的呢?如果没有移动构造函数,构造一个使用 std::move() 的对象如何退回到复制构造函数?变量 b 的绑定究竟是如何发生的?

struct Test {
  // Default constructor
  Test() {
    std::cout << "Constructor is called." << std::endl;
    mValue = 0;
  }
  
  // Copy constructor
  Test(const Test& rhs) {
    std::cout << "Copy Constructor is called." << std::endl;
    mName = rhs.mName;
    mValue = rhs.mValue;
  }
    
  std::string mName;
  int mValue;
};

int main() {
  Test a;
  Test b = std::move(a);
  return 0;
}

输出:

Constructor is called.
Copy Constructor is called.

类比一下。想想这段代码:

void doSomething(const int& x) {
    std::cout << "You like " << x << "? That's my favorite number!" << std::endl;
}

int main() {
    doSomething(137); // <-- Here
}

现在,关注 main 中的通话。这段代码编译和运行都很好,但是有一点奇怪。请注意 doSomething 接受了 const int&。这意味着它接收 referenceint,并且引用(通常)仅绑定到左值。但是这里的参数 137 是一个右值。给出了什么?

这样做的原因是 C++ 语言特别允许 const 左值引用绑定到右值,即使常规左值引用不能。例如:

const int& totallyLegal = 137; // Yep, that's fine!
int& whoaNotCoolMan     = 42;  // Compile error!

您可以这样做有几个原因。如果你有一个 const 左值引用,你已经承诺你可以 查看 引用的对象,但你不能 修改 它。因此,将左值引用绑定到右值是安全的,因为这样您就没有办法获取“纯值”并为其赋值。从历史上看,pre-C++11,当右值引用不存在时,这使得编写函数可以说“请以不涉及制作副本的方式将此参数传递给我”通过使用const 左值引用。

现在我们有了左值引用,这条规则引入了一些以前没有的混淆点。特别是,const T& 可以绑定到 T 类型的任何表达式的结果,即使它是 T&T&&。这就是在您的案例中选择复制构造函数的原因。

不过这里还有一个细微差别。与 C++ 编译器自动为 class 定义默认构造函数、复制构造函数和赋值运算符的方式相同,前提是您自己不这样做,C++ 编译器也可以自动定义移动构造函数。然而,有一条规则说,如果一个类型有一个 user-defined 复制构造函数,那么编译器将不会为你生成一个移动构造函数。所以你的问题的完整答案是“你的复制构造函数的存在意味着没有定义移动构造函数,并且由于复制构造函数采用 const 左值引用,它将绑定到右值和左值。”