明确默认的移动构造函数有什么作用?
What does explicitly-defaulted move constructor do?
我是 C++ 的新手,需要一些关于移动构造函数的帮助。我有一些只能移动的对象,每个对象都有不同的行为,但它们都有一个句柄 int id
,所以我尝试使用继承对它们进行建模,here 的代码
#include <iostream>
#include <vector>
class Base {
protected:
int id;
Base() : id(0) { std::cout << "Base() called " << id << std::endl; }
virtual ~Base() {}
Base(const Base&) = delete;
Base& operator=(const Base&) = delete;
Base(Base&& other) noexcept = default;
Base& operator=(Base&& other) noexcept = default;
};
class Foo : public Base {
public:
Foo(int id) {
this->id = id;
std::cout << "Foo() called " << id << std::endl;
}
~Foo() { std::cout << "~Foo() called " << id << std::endl; }
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
Foo(Foo&& other) noexcept = default;
Foo& operator=(Foo&& other) noexcept = default;
};
int main() {
std::vector<Foo> foos;
for (int i = 33; i < 35; i++) {
auto& foo = foos.emplace_back(i);
}
std::cout << "----------------------------" << std::endl;
return 0;
}
每个派生 class 都有一个使用 id
销毁对象的特定析构函数(如果 id
为 0,它什么都不做),我需要为每个派生类型定义它。在这种情况下,编译器不会为我生成隐式声明的 copy/move ctors,所以我必须明确地让它只移动以遵循五的规则,但我不明白 =default
移动 ctor。
构造第二个 foo(34)
时,向量 foos
重新分配内存并将第一个 foo(33)
移动到新分配,但是,我看到这个的源和目标移动操作的 id 为 33,因此移动后,foo(33)
被销毁,在向量中留下无效的 foo
对象。在下面的输出中,我也没有看到第三个 ctor 调用,那么 foo(33)
到底被交换了什么?一个 ID 为 33 的空对象?那个 33 从哪里来,来自副本?但我已经明确删除了复制构造函数。
Base() called 0
Foo() called 33
Base() called 0
Foo() called 34
~Foo() called 33 <---- why 33?
----------------------------
~Foo() called 33
~Foo() called 34
现在,如果我改为手动定义移动构造函数:
class Foo : public Base {
public:
......
// Foo(Foo&& other) noexcept = default;
// Foo& operator=(Foo&& other) noexcept = default;
Foo(Foo&& other) noexcept { *this = std::move(other); }
Foo& operator=(Foo&& other) noexcept {
if (this != &other) {
std::swap(id, other.id);
}
return *this;
}
}
Base() called 0
Foo() called 33
Base() called 0
Foo() called 34
Base() called 0 <-- base call
~Foo() called 0 <-- now it's 0
----------------------------
~Foo() called 33
~Foo() called 34
这次明明是用base(0)
对象交换foo(33)
,在id 0
销毁后,我的foo
对象仍然有效。那么默认的move ctor和我自己的move ctor有什么区别呢?
据我所知,我几乎不需要手动定义我的移动构造函数主体和移动赋值运算符,除非我直接在堆上分配内存。大多数时候我只会使用原始数据类型,例如 int
、float
或智能指针和本机支持 std::swap
的 STL 容器,所以我认为我会很好使用 =default
将 ctors 移动到各处并让编译器按成员进行交换,这似乎是错误的?也许我应该总是为每个 class 定义我自己的 move ctor?如何确保交换的对象处于可以安全销毁的干净空状态?
What does explicitly-defaulted move constructor do?
编译器生成的移动构造函数移动每个子对象。
so after the move, foo(33) is destroyed, leaving an invalid foo object in the vector.
销毁moved-from“foo(33)”对向量中保留的moved-to“foo(33)”对象没有影响;显示的析构函数不会执行任何会导致剩余对象变为“无效”的操作。
a null object that somehow has an id of 33?
'Null'是一个指针的状态。它对 class 实例没有概念意义 - 除非 class 模拟指针包装器,在这种情况下,您可以认为 class 实例在概念上为 null它包装为空;但这不适用于示例 class.
Where does that 33 come from, from copy?
你用 33 初始化了 int;这就是 33 的来源。 moved-to 对象也有 33,因为它是从值为 33 的整数中移出的。
In the output below, I also didn't see a third ctor call,
您没有看到移动构造函数的输出,因为默认移动构造函数不输出任何内容。
So what's the difference between the defaulted move ctor and my own move ctor?
默认移动构造函数通过从源的每个子对象移动来初始化所有子对象。相比之下,您的移动构造函数默认初始化子对象,然后委托给交换 id
.
的用户定义的移动赋值
为了理解差异,如果手动编写默认移动构造函数可能会有所帮助:
Foo(Foo&& other) noexcept
: id{std::move(other.id)} {}
Perhaps I should always define my own move ctor for every single class?
没有。大多数时候你应该定义 none 的特殊成员函数。此经验法则称为 0 规则。
但请记住,大部分时间与所有时间并不相同。有时——尽管很少——你确实需要一个用户定义的析构函数,在那些情况下,大多数时候你还需要用户定义的复制和移动构造函数和赋值运算符。这被称为 5 法则(在语言中引入移动语义之前也称为 3 法则)。
How can I ensure the swapped object is in a clean null state that can be safely destructed?
第一步是指定“干净的空状态”的含义。
如果存在不能被销毁的状态,那么通常最好指定一个class不变量,作为所有成员函数的post条件始终成立,时刻保证安全.保持这种不变性通常需要用户定义的特殊成员函数。
What does explicitly-defaulted move constructor do?
在自己的实现中将moved from对象中的相应成员变量变为xvalues. This is the same as if you use std::move
后,会初始化其成员变量:
Base(Base&& other) noexcept : id(std::move(other.id)) {}
请注意,对于 int
等基本类型,这与复制值没有区别。如果您希望移动的源对象将其 id
设置为 0
,请使用 std::exchange
:
Base(Base&& other) noexcept : id(std::exchange(other.id, 0)) {}
Base& operator=(Base&& other) noexcept {
if(this != &other) id = std::exchange(other.id, 0);
return *this;
}
这样,Foo
移动构造函数和移动赋值运算符就可以 default
编辑并“做正确的事”。
建议:我认为 Base
会受益于可以直接初始化 id
的构造函数。示例:
Base(int Id) : id(Id) {
std::cout << "Base() called " << id << std::endl;
}
Base() : Base(0) {} // delegate to the above
现在,以 id 作为参数的 Foo
构造函数可能如下所示:
Foo(int Id) : Base(Id) {
std::cout << "Foo() called " << id << std::endl;
}
我是 C++ 的新手,需要一些关于移动构造函数的帮助。我有一些只能移动的对象,每个对象都有不同的行为,但它们都有一个句柄 int id
,所以我尝试使用继承对它们进行建模,here 的代码
#include <iostream>
#include <vector>
class Base {
protected:
int id;
Base() : id(0) { std::cout << "Base() called " << id << std::endl; }
virtual ~Base() {}
Base(const Base&) = delete;
Base& operator=(const Base&) = delete;
Base(Base&& other) noexcept = default;
Base& operator=(Base&& other) noexcept = default;
};
class Foo : public Base {
public:
Foo(int id) {
this->id = id;
std::cout << "Foo() called " << id << std::endl;
}
~Foo() { std::cout << "~Foo() called " << id << std::endl; }
Foo(const Foo&) = delete;
Foo& operator=(const Foo&) = delete;
Foo(Foo&& other) noexcept = default;
Foo& operator=(Foo&& other) noexcept = default;
};
int main() {
std::vector<Foo> foos;
for (int i = 33; i < 35; i++) {
auto& foo = foos.emplace_back(i);
}
std::cout << "----------------------------" << std::endl;
return 0;
}
每个派生 class 都有一个使用 id
销毁对象的特定析构函数(如果 id
为 0,它什么都不做),我需要为每个派生类型定义它。在这种情况下,编译器不会为我生成隐式声明的 copy/move ctors,所以我必须明确地让它只移动以遵循五的规则,但我不明白 =default
移动 ctor。
构造第二个 foo(34)
时,向量 foos
重新分配内存并将第一个 foo(33)
移动到新分配,但是,我看到这个的源和目标移动操作的 id 为 33,因此移动后,foo(33)
被销毁,在向量中留下无效的 foo
对象。在下面的输出中,我也没有看到第三个 ctor 调用,那么 foo(33)
到底被交换了什么?一个 ID 为 33 的空对象?那个 33 从哪里来,来自副本?但我已经明确删除了复制构造函数。
Base() called 0
Foo() called 33
Base() called 0
Foo() called 34
~Foo() called 33 <---- why 33?
----------------------------
~Foo() called 33
~Foo() called 34
现在,如果我改为手动定义移动构造函数:
class Foo : public Base {
public:
......
// Foo(Foo&& other) noexcept = default;
// Foo& operator=(Foo&& other) noexcept = default;
Foo(Foo&& other) noexcept { *this = std::move(other); }
Foo& operator=(Foo&& other) noexcept {
if (this != &other) {
std::swap(id, other.id);
}
return *this;
}
}
Base() called 0
Foo() called 33
Base() called 0
Foo() called 34
Base() called 0 <-- base call
~Foo() called 0 <-- now it's 0
----------------------------
~Foo() called 33
~Foo() called 34
这次明明是用base(0)
对象交换foo(33)
,在id 0
销毁后,我的foo
对象仍然有效。那么默认的move ctor和我自己的move ctor有什么区别呢?
据我所知,我几乎不需要手动定义我的移动构造函数主体和移动赋值运算符,除非我直接在堆上分配内存。大多数时候我只会使用原始数据类型,例如 int
、float
或智能指针和本机支持 std::swap
的 STL 容器,所以我认为我会很好使用 =default
将 ctors 移动到各处并让编译器按成员进行交换,这似乎是错误的?也许我应该总是为每个 class 定义我自己的 move ctor?如何确保交换的对象处于可以安全销毁的干净空状态?
What does explicitly-defaulted move constructor do?
编译器生成的移动构造函数移动每个子对象。
so after the move, foo(33) is destroyed, leaving an invalid foo object in the vector.
销毁moved-from“foo(33)”对向量中保留的moved-to“foo(33)”对象没有影响;显示的析构函数不会执行任何会导致剩余对象变为“无效”的操作。
a null object that somehow has an id of 33?
'Null'是一个指针的状态。它对 class 实例没有概念意义 - 除非 class 模拟指针包装器,在这种情况下,您可以认为 class 实例在概念上为 null它包装为空;但这不适用于示例 class.
Where does that 33 come from, from copy?
你用 33 初始化了 int;这就是 33 的来源。 moved-to 对象也有 33,因为它是从值为 33 的整数中移出的。
In the output below, I also didn't see a third ctor call,
您没有看到移动构造函数的输出,因为默认移动构造函数不输出任何内容。
So what's the difference between the defaulted move ctor and my own move ctor?
默认移动构造函数通过从源的每个子对象移动来初始化所有子对象。相比之下,您的移动构造函数默认初始化子对象,然后委托给交换 id
.
为了理解差异,如果手动编写默认移动构造函数可能会有所帮助:
Foo(Foo&& other) noexcept
: id{std::move(other.id)} {}
Perhaps I should always define my own move ctor for every single class?
没有。大多数时候你应该定义 none 的特殊成员函数。此经验法则称为 0 规则。
但请记住,大部分时间与所有时间并不相同。有时——尽管很少——你确实需要一个用户定义的析构函数,在那些情况下,大多数时候你还需要用户定义的复制和移动构造函数和赋值运算符。这被称为 5 法则(在语言中引入移动语义之前也称为 3 法则)。
How can I ensure the swapped object is in a clean null state that can be safely destructed?
第一步是指定“干净的空状态”的含义。
如果存在不能被销毁的状态,那么通常最好指定一个class不变量,作为所有成员函数的post条件始终成立,时刻保证安全.保持这种不变性通常需要用户定义的特殊成员函数。
What does explicitly-defaulted move constructor do?
在自己的实现中将moved from对象中的相应成员变量变为xvalues. This is the same as if you use std::move
后,会初始化其成员变量:
Base(Base&& other) noexcept : id(std::move(other.id)) {}
请注意,对于 int
等基本类型,这与复制值没有区别。如果您希望移动的源对象将其 id
设置为 0
,请使用 std::exchange
:
Base(Base&& other) noexcept : id(std::exchange(other.id, 0)) {}
Base& operator=(Base&& other) noexcept {
if(this != &other) id = std::exchange(other.id, 0);
return *this;
}
这样,Foo
移动构造函数和移动赋值运算符就可以 default
编辑并“做正确的事”。
建议:我认为 Base
会受益于可以直接初始化 id
的构造函数。示例:
Base(int Id) : id(Id) {
std::cout << "Base() called " << id << std::endl;
}
Base() : Base(0) {} // delegate to the above
现在,以 id 作为参数的 Foo
构造函数可能如下所示:
Foo(int Id) : Base(Id) {
std::cout << "Foo() called " << id << std::endl;
}