了解 copy/move 构造函数和运算符之间的推理

Understanding the reasoning between copy/move constructors and operators

我试图通过一个简单的自制示例来掌握右值引用和移动语义,但我无法理解特定部分。我创建了以下 class:

class A {
public:
    A(int a) {
        cout << "Def constructor" << endl;
    }

    A(const A& var) {
        cout << "Copy constructor" << endl;
    }

    A(A&& var) {
        cout << "Move constructor" << endl;
    }

    A& operator=(const A& var) {
        cout << "Copy Assignment" << endl;
        return *this;
    }

    A& operator=(A&& var) {
        cout << "Move Assignment" << endl;
        return *this;
    }
};

我尝试了以下实验,看看是否可以预测 constructors/operators 将如何被调用:

  1. A a1(1) - 将调用默认构造函数。 预测
  2. A a2 = a1 - 复制构造函数将被调用。 预测
  3. a1 = a2 - 将调用复制赋值运算符。 预测

现在,我创建了一个简单的函数,它只是 returns 一个 A 对象。

A helper() {
   return A(1);
}
  1. A a3 = helper() - 默认构造函数将被调用 为了创建助手 returns 的对象。此举 由于 RVO,不会调用构造函数。 预测
  2. a3 = helper() - 默认构造函数将被调用 为了创建助手 returns 的对象。然后,移动 赋值运算符将被调用。 预测

现在是我不明白的部分。我创建了另一个完全没有意义的函数。它按值获取一个 A 对象,它只是 returns 它。

A helper_alt(A a) {
    return a;
}
  1. A a4 = helper_alt(a1) - 这将调用复制构造函数,以 实际上复制函数中的对象a1然后移动 构造函数。 预测
  2. a4 = helper_alt(a1) - 这将调用复制构造函数,以 实际上在函数中复制对象 a1 然后我认为 如我所见,移动赋值运算符将被称为 BUT, 首先,调用移动构造函数,然后调用移动赋值 运营商被称为。 不知道

如果我说的有什么不对的地方,或者你觉得我可能有什么不明白的地方,请随时指正。

我的实际问题在最后一个案例中,为什么先调用移动构造函数然后调用移动赋值运算符,而不是仅仅调用移动赋值运算符?

恭喜你,发现了一个C++核心问题!

关于您在示例代码中看到的行为,仍有很多讨论。

有如下建议:

A&& helper_alt(A a) {
    std::cout << ".." << std::endl;
    return std::move(a);
}

这会做你想做的,只需使用移动赋值,但会发出来自 g++ 的警告“警告:返回对局部变量 'a' 的引用”,即使该变量立即超出范围。

其他人已经发现了这个问题,这已经成为一个 c++ standard language core issue

有趣的是,这个问题在 2010 年就已经被发现,但直到现在才得到解决...

回答你的问题“在最后一个例子中,为什么先调用移动构造函数然后调用移动赋值运算符,而不是仅仅调用移动赋值运算符?" 是,C++ 委员会直到现在也没有答案。准确地说,有一个建议的解决方案,这个被接受,但直到现在还不是语言的一部分。

发件人:Comment Status

Amend paragraph 34 to explicitly exclude function parameters from copy elision. Amend paragraph 35 to include function parameters as eligible for move-construction.

考虑下面的例子。我使用 -fno-elide-constructors 标志编译示例代码以防止 RVO 优化:
g++ -fno-elide-constructors -o test test.cpp

#include<iostream>

using namespace std;

class A {
public:
    A(int a) {
        cout << "Def constructor" << endl;
    }

    A(const A& var) {
        cout << "Copy constructor" << endl;
    }

    A(A&& var) {
        cout << "Move constructor" << endl;
    }

    A& operator=(const A& var) {
        cout << "Copy Assignment" << endl;
        return *this;
    }

    A& operator=(A&& var) {
        cout << "Move Assignment" << endl;
        return *this;
    }
};

A a_global(1);

A helper_alt(A a) {
    return a;
}

A helper_a_local(A a) {
    A x(1);
    return x;
}

A helper_a_global(A a) {
    return a_global;
}

int main(){
    A a1(1);
    A a4(4);
    std::cout << "================= helper_alt(a1) ==================" << std::endl;
    a4 = helper_alt(a1);
    std::cout << "=============== helper_a_local()   ================" << std::endl;
    a4 = helper_a_local(a1);
    std::cout << "=============== helper_a_global()  ================" << std::endl;
    a4 = helper_a_global(a1);
    return 0;
}

这将导致以下输出:

Def constructor
Def constructor
Def constructor
================= helper_alt(a1) ==================
Copy constructor
Move constructor
Move Assignment
=============== helper_a_local()   ================
Copy constructor
Def constructor
Move constructor
Move Assignment
=============== helper_a_global()  ================
Copy constructor
Copy constructor
Move Assignment

简而言之,当 return 类型不是引用时,C++ 构造一个新的临时对象 (rvalue),这会导致根据值类别和returned 对象的生命周期。
无论如何,我认为调用构造函数背后的逻辑是你没有使用引用,并且 returned 身份应该首先被解释,通过复制或移动构造函数,取决于 returned value category 或 return 对象的生命周期。再举个例子:

A helper_move_vs_copy(A a) {
    // Call the Copy Constructor
    A b = a;
    // Call the Move Constructor, Due to the end of 'a' lifetime
    return a;
}

int main(){
    A a1(1);
    A a2(4);
    std::cout << "=============== helper_move_vs_copy()  ================" << std::endl;
    helper_move_vs_copy(a1);
    return 0;
}

输出:

Def constructor
Def constructor
=============== helper_move_vs_copy()  ================
Copy constructor
Copy constructor
Move constructor

来自cppreference

an xvalue (an “eXpiring” value) is a glvalue that denotes an object whose resources can be reused;

最后,RVO 的工作是通过优化代码来减少不必要的移动和复制,这甚至可以为基本程序员生成优化的二进制文件!