以下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 1
和 Case 1.5
相同的跟踪输出。
问题1:比较case 2和case 1或case 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 在传递给函数时表现得像左值引用。
c++11中的三个语句并不完全相同。
情况 2 需要 C++17 之前的 move 构造
该语言要求 X x = X{}
的代码存在 move-constructor -- 否则代码将无法编译。
例如,使用 Person
class 定义如下:
class Person{
public:
...
Person(Person&&) = delete;
...
};
将无法编译像这样的语句:
Person p = Person{}; // Case 2
上的示例
注意: 以上代码在 c++17 及以后完全有效,因为措辞更改允许直接在目标地址中构造对象,即使不可移动且不可复制(这就是人们常说的“保证复制省略”)。
案例 3 是临时文件的生命周期延长
第三种情况是临时构造,其生命周期通过绑定到 rvalue-reference 来延长。在某些情况下临时对象的生命周期可以延长,因为它们绑定到右值引用或 const
左值引用。例如,以下两个构造是等价的,因为它们绑定到临时的生命周期:
Person&& p3_1 = Person{};
const Person& p3_2 = Person{};
就作用域规则而言,它与任何其他自动变量具有相同的生命周期(例如,它将以与 Person person{}
相同的方式在作用域末尾调用析构函数)。然而,至少在 c++11 中,构造可以完成的工作与 Person p2 = Person{}
完全不同,因为此代码 将始终编译 即使移动构造函数不存在(因为这是引用绑定)。
例如,让我们考虑一个不可移动、不可复制的类型,如 std::mutex
。在 C++17 中,编写以下代码是有效的:
std::mutex mutex = std::mutex{};
但在 C++11 中无法编译。但是,您可以自由编写:
std::mutex&& mutex = std::mutex{};
它创建一个临时变量并将其绑定到一个引用,该引用的生命周期与此时构造的任何作用域变量相同。
注意: 故意传播临时对象的生命周期通常不是故意要做的事情,但在 C++17 之前,这是实现 almost-always-auto 带有不可移动对象的语法。例如,上面的内容可以改写为:auto&& mutex = std::mutex{}
据我了解,以下是相同的:
Person p{}; // Case 1
Person p = {}; // Case 1.5
我注意到了
Person p = Person{}; // Case 2
产生与上面的 Case 1
和 Case 1.5
相同的跟踪输出。
问题1:比较case 2和case 1或case 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 在传递给函数时表现得像左值引用。
c++11中的三个语句并不完全相同。
情况 2 需要 C++17 之前的 move 构造
该语言要求 X x = X{}
的代码存在 move-constructor -- 否则代码将无法编译。
例如,使用 Person
class 定义如下:
class Person{
public:
...
Person(Person&&) = delete;
...
};
将无法编译像这样的语句:
Person p = Person{}; // Case 2
上的示例
注意: 以上代码在 c++17 及以后完全有效,因为措辞更改允许直接在目标地址中构造对象,即使不可移动且不可复制(这就是人们常说的“保证复制省略”)。
案例 3 是临时文件的生命周期延长
第三种情况是临时构造,其生命周期通过绑定到 rvalue-reference 来延长。在某些情况下临时对象的生命周期可以延长,因为它们绑定到右值引用或 const
左值引用。例如,以下两个构造是等价的,因为它们绑定到临时的生命周期:
Person&& p3_1 = Person{};
const Person& p3_2 = Person{};
就作用域规则而言,它与任何其他自动变量具有相同的生命周期(例如,它将以与 Person person{}
相同的方式在作用域末尾调用析构函数)。然而,至少在 c++11 中,构造可以完成的工作与 Person p2 = Person{}
完全不同,因为此代码 将始终编译 即使移动构造函数不存在(因为这是引用绑定)。
例如,让我们考虑一个不可移动、不可复制的类型,如 std::mutex
。在 C++17 中,编写以下代码是有效的:
std::mutex mutex = std::mutex{};
但在 C++11 中无法编译。但是,您可以自由编写:
std::mutex&& mutex = std::mutex{};
它创建一个临时变量并将其绑定到一个引用,该引用的生命周期与此时构造的任何作用域变量相同。
注意: 故意传播临时对象的生命周期通常不是故意要做的事情,但在 C++17 之前,这是实现 almost-always-auto 带有不可移动对象的语法。例如,上面的内容可以改写为:auto&& mutex = std::mutex{}