所以,在观看这个精彩的演讲关于右值引用之后,我认为如果每个类都拥有一个"移动构造函数"template<class T> MyClass(T&& other)
(编辑)和一个"移动赋值运算符"template<class T> MyClass& operator=(T&& other)
(正如Philipp在他的回答中指出的),如果它具有动态分配的成员或者通常存储指针,那么它将受益匪浅。就像你应该有一个复制构造函数、赋值运算符和析构函数一样,如果应用到之前提到的点。
所以,在观看这个精彩的演讲关于右值引用之后,我认为如果每个类都拥有一个"移动构造函数"template<class T> MyClass(T&& other)
(编辑)和一个"移动赋值运算符"template<class T> MyClass& operator=(T&& other)
(正如Philipp在他的回答中指出的),如果它具有动态分配的成员或者通常存储指针,那么它将受益匪浅。就像你应该有一个复制构造函数、赋值运算符和析构函数一样,如果应用到之前提到的点。
我认为三五法则现在应该改成三四五法则:
每个类都应该显式地定义以下特殊成员函数集中的一个:
- 没有
- 析构函数,拷贝构造函数,拷贝赋值运算符
此外,每个显式定义了析构函数的类都可以显式定义移动构造函数和/或移动赋值运算符。
通常情况下,以下几组特殊成员函数是合理的:
- 没有(对于许多简单的类,在隐式生成的特殊成员函数正确且快速时)
- 析构函数,拷贝构造函数,拷贝赋值运算符(在这种情况下,该类将不可移动)
- 析构函数,移动构造函数,移动赋值运算符(在这种情况下,该类将不可复制,适用于底层资源不可复制的资源管理类)
- 析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数(由于拷贝省略,如果拷贝赋值运算符以值传递其参数,则没有开销)
- 析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符
注意:
特别地,以下是完全有效的 C++03 多态基类:
class C {
virtual ~C() { } // allow subtype polymorphism
};
应该重写如下:
class C {
C(const C&) = default; // Copy constructor
C(C&&) = default; // Move constructor
C& operator=(const C&) = default; // Copy assignment operator
C& operator=(C&&) = default; // Move assignment operator
virtual ~C() { } // Destructor
};
有点烦人,但很可能比另一种选择更好(在这种情况下,仅生成用于复制的特殊成员函数,而没有移动功能)。
与“三大法则”不同的是,不显式声明移动构造函数和移动赋值运算符通常是可以的,但通常不如效率高。如上所述,只有在没有显式声明复制构造函数、复制赋值运算符或析构函数时,才会生成移动构造函数和移动赋值运算符。这不对称于传统的C++03行为,即自动生成复制构造函数和复制赋值运算符,但更加安全。因此,定义移动构造函数和移动赋值运算符的可能性非常有用,并且创造了新的可能性(纯移动类),但遵循“三大法则”的类仍然是可以的。
对于资源管理类,如果底层资源无法复制,则可以将复制构造函数和复制赋值运算符定义为已删除的(这被视为定义)。通常您仍需要移动构造函数和移动赋值运算符。复制和移动赋值运算符通常使用swap
实现,就像C++03一样。谈论swap
;如果我们已经有了移动构造函数和移动赋值运算符,专门化std::swap
将变得不重要,因为通用的std::swap
会使用移动构造函数和移动赋值运算符(如果可用,则应该足够快)。
不是针对资源管理(即没有非空析构函数)或子类型多态(即没有虚拟析构函数)的类应该不声明五个特殊成员函数;它们都将被自动生成并正确且快速地工作。
struct C { virtual ~C() = default; };
,这是最简洁的选项。n3242中的禁止(“-它不得是虚拟的”)在n3290中已经不存在了,而且GCC现在允许使用,之前是不允许的。 - Luc Dantonstd::unique_ptr<>
жҲҗе‘ҳпјҢ并еёҢжңӣдҪҝе…¶еҸҜ移еҠЁдҪҶдёҚеҸҜеӨҚеҲ¶пјҢйӮЈиҜҘжҖҺд№ҲеҠһе‘ўпјҹжӮЁжҳҜеҗҰеҸҜд»ҘжңҖз»ҲеҸӘжңүдёӨдёӘжҲҗе‘ҳпјҲ移еҠЁиөӢеҖјпјҢ移еҠЁжһ„йҖ еҮҪж•°пјүпјҹпјҲжҠұжӯүпјҢжҲ‘дёҚжҳҜж•…ж„ҸеҚ–еј„пјҢиҝҷжҳҜжҲ‘йҒҮеҲ°зҡ„дёҖдёӘе®һйҷ…жғ…еҶөпјҒпјүйЎәдҫҝиҜҙдёҖеҸҘпјҢеҫҲжЈ’зҡ„зӯ”жЎҲпјҢжҲ‘зӮ№дәҶ+1并еҠ жҳҹж Үи®°дәҶгҖӮ - kfmfe04我无法相信没有人链接到这篇文章。
基本上,这篇文章提倡使用“零规则”。 引用整篇文章对我来说不太合适,但我认为这是主要观点:
有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应仅处理所有权。 其他类不应具有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符。
此外,以下内容在我看来也很重要:
常见的“包装所有权”的类已包含在标准库中:
std::unique_ptr
和std::shared_ptr
。通过使用定制的删除器对象,两者都变得足够灵活,可以管理几乎任何类型的资源。
是的,我认为为这些类提供一个移动构造函数会很不错,但请记住:
这只是一种优化。
仅实现拷贝构造函数、赋值运算符或析构函数中的一个或两个可能会导致错误,而没有移动构造函数只会潜在地降低性能。
移动构造函数并不总是可以直接使用。
有些类始终分配它们的指针,因此这些类在析构函数中总是要删除它们的指针。在这些情况下,您需要添加额外的检查来判断它们的指针是否已被分配或已移动(现在为 null)。
unique_ptr
)的实现中也非常重要。 - Puppyauto_ptr
遵循了三五法则,而 unique_ptr
明显没有遵循五法则。 - Puppyboost::noncopyable
)可以被称为遵守三法则。 (否则,我们必须引入不同的术语,例如“大一和小二法则”)。 - Philipptemplate<class T>
class unique_ptr
{
T* ptr;
public:
explicit unique_ptr(T* p=0) : ptr(p) {}
~unique_ptr();
unique_ptr(unique_ptr&&);
unique_ptr& operator=(unique_ptr&&);
};
template<class T>
class scoped_ptr
{
T* ptr;
public:
explicit scoped_ptr(T* p=0) : ptr(p) {}
~scoped_ptr();
};
现在预计C++0x编译器会发出警告,提醒可能会出现错误的编译器生成复制操作。在这里,三法则很重要,应该受到尊重。在这种情况下发出警告是完全合适的,并给用户处理错误的机会。我们可以通过删除函数来解决这个问题:
template<class T>
class scoped_ptr
{
T* ptr;
public:
explicit scoped_ptr(T* p=0) : ptr(p) {}
~scoped_ptr();
scoped_ptr(scoped_ptr const&) = delete;
scoped_ptr& operator=(scoped_ptr const&) = delete;
};
unique_ptr
的复制构造函数和复制赋值运算符定义为已删除 - 有人知道为什么吗? - Philippunique_ptr
具有用户声明的移动构造函数和移动赋值运算符,因此我认为即使应用了N3126规则,也不需要用户声明复制构造函数和复制赋值运算符。虽然不是很重要,但由于标准库类使用的约定可能被解释为最佳实践。 - Philipp通常情况下,确实是这样的,规则从三变为五,包括移动赋值运算符和移动构造函数。但是,并不是所有的类都可以复制和移动,有些只能移动,有些只能复制。
std::complex
要关心右值引用? - Motti简单来说,只需记住以下几点。
0号规则:
类没有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符。
3号规则: 如果您实现了任何一个自定义版本,您需要实现所有这些版本。
析构函数、复制构造函数、复制赋值运算符
5号规则: 如果您实现了自定义移动构造函数或移动赋值运算符,则需要定义所有5个版本。需要用于移动语义。
析构函数、复制构造函数、复制赋值运算符、移动构造函数、移动赋值运算符
四个半的规则: 与5号规则相同,但使用复制和交换惯用语。通过包含swap方法,复制赋值和移动赋值合并为一个赋值运算符。
析构函数、复制构造函数、移动构造函数、赋值、交换(半部分)
参考资料:
https://www.linkedin.com/learning/c-plus-plus-advanced-topics/rule-of-five?u=67551194 https://en.cppreference.com/w/cpp/language/rule_of_three