以下3种定义对象的方式是否相同?

Are the following 3 ways to define objects identical?

据我了解,以下是相同的:

Person p{}; // Case 1
Person p = {}; // Case 1.5

我注意到了

Person p = Person{}; // Case 2

产生与上面的 Case 1Case 1.5 相同的跟踪输出。

Person p{};            // Case 1
Person p = Person{};   // Case 2
Person&& p = Person{}; // Case 3

- 关于构造将如何发生,以及构造变量的行为方式;但是 No 关于变量的类型。

编译器在任何这些情况下都不使用赋值,即它只有您的程序 default-construct。您可以使用此代码来验证:

#include <iostream>

struct Person {
    Person& operator=(Person&) {
       std::cout << "Assignment: operator=(Person&)\n";  return *this; 
    }
    Person& operator=(Person&&) { 
       std::cout << "Move assignment: operator=(Person&&)\n";  return *this; 
    }
    Person(const Person&) { std::cout << "Copy ctor: Person(Person&)\n"; }
    Person(Person&&) { std::cout << "Move ctor: Person(Person&&)\n"; }
    Person() { std::cout << "Default ctor: Person()\n"; }
};

int main() {
    std::cout << "P1:\n";
    Person p1{};
    std::cout << "Address of P1: " << &p1 << '\n';
    std::cout << "P2:\n";
    Person p2 = Person{};
    std::cout << "Address of P2: " << &p2 << '\n';
    std::cout << "P3:\n";
    Person&& p3 = Person{};
    std::cout << "Address of P3: " << &p3 << '\n';
}

GodBolt 上查看。

第三个语句的行为让我有点惊讶;我实际上虽然编译器可能会完全拒绝它。无论如何 - 请不要像那样声明右值引用。这让读者感到困惑 - 甚至对我来说也是如此,而且几乎肯定不是你想做的事情。我确信 p3 的行为类似于右值引用;但是 - 事实并非如此,显然:尽管具有类型 Person&&,但它 will 在传递给函数时表现得像左值引用。

中的三个语句并不完全相同。

情况 2 需要 C++17 之前的 move 构造

该语言要求 X x = X{} 的代码存在 move-constructor -- 否则代码将无法编译。

例如,使用 Person class 定义如下:

class Person{
public:
    ...
    Person(Person&&) = delete;
    ...
};

将无法编译像这样的语句:

Person p = Person{}; // Case 2

compiler explorer

上的示例

注意: 以上代码在 及以后完全有效,因为措辞更改允许直接在目标地址中构造对象,即使不可移动且不可复制(这就是人们常说的“保证复制省略”)。

案例 3 是临时文件的生命周期延长

第三种情况是临时构造,其生命周期通过绑定到 rvalue-reference 来延长。在某些情况下临时对象的生命周期可以延长,因为它们绑定到右值引用或 const 左值引用。例如,以下两个构造是等价的,因为它们绑定到临时的生命周期:

Person&& p3_1 = Person{};
const Person& p3_2 = Person{};

就作用域规则而言,它与任何其他自动变量具有相同的生命周期(例如,它将以与 Person person{} 相同的方式在作用域末尾调用析构函数)。然而,至少在 中,构造可以完成的工作与 Person p2 = Person{} 完全不同,因为此代码 将始终编译 即使移动构造函数不存在(因为这是引用绑定)。

例如,让我们考虑一个不可移动、不可复制的类型,如 std::mutex。在 C++17 中,编写以下代码是有效的:

std::mutex mutex = std::mutex{};

但在 C++11 中无法编译。但是,您可以自由编写:

std::mutex&& mutex = std::mutex{};

它创建一个临时变量并将其绑定到一个引用,该引用的生命周期与此时构造的任何作用域变量相同。

示例 compiler explorer.

注意: 故意传播临时对象的生命周期通常不是故意要做的事情,但在 C++17 之前,这是实现 almost-always-auto 带有不可移动对象的语法。例如,上面的内容可以改写为:auto&& mutex = std::mutex{}