Difference between revisions of "cpp/language/rule of three"
(The labels for the Rule of Three code were swapped.) |
m (→Rule of five: V. Add missing "return *this" to `operator=` body.) |
||
(11 intermediate revisions by 8 users not shown) | |||
Line 3: | Line 3: | ||
===Rule of three=== | ===Rule of three=== | ||
− | If a class requires a user-defined {{rlp|destructor}}, a user-defined {{rlp|copy constructor}}, or a user-defined {{rlp| | + | If a class requires a user-defined {{rlp|destructor}}, a user-defined {{rlp|copy constructor}}, or a user-defined {{rlp|as operator|copy assignment operator}}, it almost certainly requires all three. |
Because C++ copies and copy-assigns objects of user-defined types in various situations (passing/returning by value, manipulating a container, etc), these special member functions will be called, if accessible, and if they are not user-defined, they are implicitly-defined by the compiler. | Because C++ copies and copy-assigns objects of user-defined types in various situations (passing/returning by value, manipulating a container, etc), these special member functions will be called, if accessible, and if they are not user-defined, they are implicitly-defined by the compiler. | ||
− | The implicitly-defined special member functions | + | The implicitly-defined special member functions should not be used if the class manages a resource whose handle does not destroy the resource themselves (raw pointer, POSIX file descriptor, etc), whose destructor does nothing and copy constructor/assignment operator only copies the value of the handle, without duplicating the underlying resource. |
− | {{example|code= | + | {{example |
+ | |code= | ||
#include <cstddef> | #include <cstddef> | ||
#include <cstring> | #include <cstring> | ||
#include <iostream> | #include <iostream> | ||
− | + | #include <utility> | |
+ | |||
class rule_of_three | class rule_of_three | ||
{ | { | ||
char* cstring; // raw pointer used as a handle to a | char* cstring; // raw pointer used as a handle to a | ||
// dynamically-allocated memory block | // dynamically-allocated memory block | ||
− | + | ||
− | rule_of_three(const char* s, std::size_t n) | + | public: |
− | : cstring(new char[n]) // allocate | + | rule_of_three(const char* s, std::size_t n) |
+ | : cstring(new char[n + 1]) // allocate | ||
{ | { | ||
std::memcpy(cstring, s, n); // populate | std::memcpy(cstring, s, n); // populate | ||
+ | cstring[n] = '\0'; // tail 0 | ||
} | } | ||
− | + | ||
− | + | ||
explicit rule_of_three(const char* s = "") | explicit rule_of_three(const char* s = "") | ||
− | : rule_of_three(s, std::strlen(s) | + | : rule_of_three(s, std::strlen(s)) |
− | + | { | |
+ | } | ||
+ | |||
~rule_of_three() // I. destructor | ~rule_of_three() // I. destructor | ||
{ | { | ||
delete[] cstring; // deallocate | delete[] cstring; // deallocate | ||
} | } | ||
− | + | ||
rule_of_three(const rule_of_three& other) // II. copy constructor | rule_of_three(const rule_of_three& other) // II. copy constructor | ||
− | : rule_of_three(other.cstring) { | + | : rule_of_three(other.cstring) |
− | + | { | |
+ | } | ||
+ | |||
rule_of_three& operator=(const rule_of_three& other) // III. copy assignment | rule_of_three& operator=(const rule_of_three& other) // III. copy assignment | ||
{ | { | ||
if (this == &other) | if (this == &other) | ||
return *this; | return *this; | ||
− | + | ||
− | + | rule_of_three temp(other); // use the copy constructor | |
− | + | std::swap(cstring, temp.cstring); // exchange the underlying resource | |
− | std:: | + | |
− | + | ||
− | + | ||
− | + | ||
return *this; | return *this; | ||
} | } | ||
− | + | ||
− | + | const char* c_str() const // accessor | |
{ | { | ||
return cstring; | return cstring; | ||
} | } | ||
}; | }; | ||
− | + | ||
int main() | int main() | ||
{ | { | ||
rule_of_three o1{"abc"}; | rule_of_three o1{"abc"}; | ||
− | std::cout << o1 << ' '; | + | std::cout << o1.c_str() << ' '; |
auto o2{o1}; // II. uses copy constructor | auto o2{o1}; // II. uses copy constructor | ||
− | std::cout << o2 << ' '; | + | std::cout << o2.c_str() << ' '; |
− | rule_of_three o3 | + | rule_of_three o3{"def"}; |
− | std::cout << o3 << ' '; | + | std::cout << o3.c_str() << ' '; |
o3 = o2; // III. uses copy assignment | o3 = o2; // III. uses copy assignment | ||
− | std::cout << o3 << '\n'; | + | std::cout << o3.c_str() << '\n'; |
− | } // | + | } // I. all destructors are called here |
|output= | |output= | ||
abc abc def abc | abc abc def abc | ||
}} | }} | ||
− | Classes that manage non-copyable resources through copyable handles may have to declare copy assignment and copy constructor private and not provide their definitions | + | Classes that manage non-copyable resources through copyable handles may have to {{rev inl|until=c++11|declare copy assignment and copy constructor {{c/core|private}} and not provide their definitions}}{{rev inl|since=c++11|define copy assignment and copy constructor as {{c|1== delete}}}}. This is another application of the rule of three: deleting one and leaving the other to be implicitly-defined typically incorrect. |
===Rule of five=== | ===Rule of five=== | ||
− | Because the presence of a user-defined ( | + | Because the presence of a user-defined (include {{c|1== default}} or {{c|1== delete}} declared) destructor, copy-constructor, or copy-assignment operator prevents implicit definition of the {{rlp|move constructor}} and the {{rlp|move operator|move assignment operator}}, any class for which move semantics are desirable, has to declare all five special member functions: |
{{source|1= | {{source|1= | ||
+ | #include <cstddef> | ||
+ | #include <cstring> | ||
+ | #include <utility> | ||
+ | |||
class rule_of_five | class rule_of_five | ||
{ | { | ||
char* cstring; // raw pointer used as a handle to a | char* cstring; // raw pointer used as a handle to a | ||
// dynamically-allocated memory block | // dynamically-allocated memory block | ||
+ | |||
public: | public: | ||
− | + | rule_of_five(const char* s, std::size_t n) | |
− | + | : cstring(new char[n + 1]) // allocate | |
− | + | { | |
− | + | std::memcpy(cstring, s, n); // populate | |
− | + | cstring[n] = '\0'; // tail 0 | |
− | + | ||
− | + | ||
− | + | ||
} | } | ||
− | + | ||
− | ~rule_of_five() | + | explicit rule_of_five(const char* s) |
+ | : rule_of_five(s, std::strlen(s)) | ||
+ | { | ||
+ | } | ||
+ | |||
+ | ~rule_of_five() // I. destructor | ||
{ | { | ||
delete[] cstring; // deallocate | delete[] cstring; // deallocate | ||
} | } | ||
− | + | ||
− | rule_of_five(const rule_of_five& other) // copy constructor | + | rule_of_five(const rule_of_five& other) // II. copy constructor |
− | : rule_of_five(other.cstring) { | + | : rule_of_five(other.cstring) |
− | + | { | |
− | rule_of_five(rule_of_five&& other) | + | } |
− | + | ||
− | + | rule_of_five& operator=(const rule_of_five& other) // III. copy assignment | |
− | rule_of_five | + | { |
+ | if (this == &other) | ||
+ | return *this; | ||
+ | |||
+ | rule_of_five temp(other); // use the copy constructor | ||
+ | std::swap(cstring, temp.cstring); // exchange the underlying resource | ||
+ | |||
+ | return *this; | ||
+ | } | ||
+ | |||
+ | rule_of_five(rule_of_five&& other) noexcept // IV. move constructor | ||
+ | : cstring(std::exchange(other.cstring, nullptr)) | ||
{ | { | ||
− | |||
} | } | ||
− | + | ||
− | rule_of_five& operator=(rule_of_five&& other) noexcept // move assignment | + | rule_of_five& operator=(rule_of_five&& other) noexcept // V. move assignment |
{ | { | ||
− | std::swap(cstring, | + | rule_of_five temp(std::move(other)); |
+ | std::swap(cstring, temp.cstring); | ||
return *this; | return *this; | ||
} | } | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
}; | }; | ||
}} | }} | ||
− | Unlike Rule of Three, failing to provide move constructor and move assignment is usually not an error, but a | + | Unlike Rule of Three, failing to provide move constructor and move assignment is usually not an error, but it will result in a loss of performance. |
===Rule of zero=== | ===Rule of zero=== | ||
Line 135: | Line 150: | ||
std::string cppstring; | std::string cppstring; | ||
public: | public: | ||
− | rule_of_zero(const std::string& arg) : cppstring(arg) {} | + | // redundant, implicitly defined is better |
+ | // rule_of_zero(const std::string& arg) : cppstring(arg) {} | ||
}; | }; | ||
}} | }} | ||
− | When a base class is intended for polymorphic use, its destructor may have to be declared public and virtual. This blocks implicit moves (and deprecates implicit copies), and so the special member functions have to be | + | When a base class is intended for polymorphic use, its destructor may have to be declared {{c/core|public}} and {{c/core|virtual}}. This blocks implicit moves (and deprecates implicit copies), and so the special member functions have to be defined as {{c|1== default}}<ref>[https://scottmeyers.blogspot.fr/2014/03/a-concern-about-rule-of-zero.html "A Concern about the Rule of Zero", Scott Meyers, 3/13/2014].</ref>. |
{{source|1= | {{source|1= | ||
class base_of_five_defaults | class base_of_five_defaults | ||
Line 151: | Line 167: | ||
}; | }; | ||
}} | }} | ||
− | However, this makes the class prone to slicing, which is why polymorphic classes often define copy as | + | However, this makes the class prone to slicing, which is why polymorphic classes often define copy as {{c|1== delete}} (see [https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c67-a-polymorphic-class-should-suppress-public-copymove C.67: A polymorphic class should suppress public copy/move] in C++ Core Guidelines), which leads to the following generic wording for the Rule of Five: |
− | :[https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c21-if-you-define-or-delete-any-copy-move-or-destructor-function-define-or-delete-them-all C.21: If you define or | + | :[https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c21-if-you-define-or-delete-any-copy-move-or-destructor-function-define-or-delete-them-all C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all.] |
===External links=== | ===External links=== |
Latest revision as of 15:24, 24 September 2024
Contents |
[edit] Rule of three
If a class requires a user-defined destructor, a user-defined copy constructor, or a user-defined copy assignment operator, it almost certainly requires all three.
Because C++ copies and copy-assigns objects of user-defined types in various situations (passing/returning by value, manipulating a container, etc), these special member functions will be called, if accessible, and if they are not user-defined, they are implicitly-defined by the compiler.
The implicitly-defined special member functions should not be used if the class manages a resource whose handle does not destroy the resource themselves (raw pointer, POSIX file descriptor, etc), whose destructor does nothing and copy constructor/assignment operator only copies the value of the handle, without duplicating the underlying resource.
#include <cstddef> #include <cstring> #include <iostream> #include <utility> class rule_of_three { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: rule_of_three(const char* s, std::size_t n) : cstring(new char[n + 1]) // allocate { std::memcpy(cstring, s, n); // populate cstring[n] = '\0'; // tail 0 } explicit rule_of_three(const char* s = "") : rule_of_three(s, std::strlen(s)) { } ~rule_of_three() // I. destructor { delete[] cstring; // deallocate } rule_of_three(const rule_of_three& other) // II. copy constructor : rule_of_three(other.cstring) { } rule_of_three& operator=(const rule_of_three& other) // III. copy assignment { if (this == &other) return *this; rule_of_three temp(other); // use the copy constructor std::swap(cstring, temp.cstring); // exchange the underlying resource return *this; } const char* c_str() const // accessor { return cstring; } }; int main() { rule_of_three o1{"abc"}; std::cout << o1.c_str() << ' '; auto o2{o1}; // II. uses copy constructor std::cout << o2.c_str() << ' '; rule_of_three o3{"def"}; std::cout << o3.c_str() << ' '; o3 = o2; // III. uses copy assignment std::cout << o3.c_str() << '\n'; } // I. all destructors are called here
Output:
abc abc def abc
Classes that manage non-copyable resources through copyable handles may have to declare copy assignment and copy constructor private and not provide their definitions(until C++11)define copy assignment and copy constructor as = delete(since C++11). This is another application of the rule of three: deleting one and leaving the other to be implicitly-defined typically incorrect.
[edit] Rule of five
Because the presence of a user-defined (include = default or = delete declared) destructor, copy-constructor, or copy-assignment operator prevents implicit definition of the move constructor and the move assignment operator, any class for which move semantics are desirable, has to declare all five special member functions:
#include <cstddef> #include <cstring> #include <utility> class rule_of_five { char* cstring; // raw pointer used as a handle to a // dynamically-allocated memory block public: rule_of_five(const char* s, std::size_t n) : cstring(new char[n + 1]) // allocate { std::memcpy(cstring, s, n); // populate cstring[n] = '\0'; // tail 0 } explicit rule_of_five(const char* s) : rule_of_five(s, std::strlen(s)) { } ~rule_of_five() // I. destructor { delete[] cstring; // deallocate } rule_of_five(const rule_of_five& other) // II. copy constructor : rule_of_five(other.cstring) { } rule_of_five& operator=(const rule_of_five& other) // III. copy assignment { if (this == &other) return *this; rule_of_five temp(other); // use the copy constructor std::swap(cstring, temp.cstring); // exchange the underlying resource return *this; } rule_of_five(rule_of_five&& other) noexcept // IV. move constructor : cstring(std::exchange(other.cstring, nullptr)) { } rule_of_five& operator=(rule_of_five&& other) noexcept // V. move assignment { rule_of_five temp(std::move(other)); std::swap(cstring, temp.cstring); return *this; } };
Unlike Rule of Three, failing to provide move constructor and move assignment is usually not an error, but it will result in a loss of performance.
[edit] Rule of zero
Classes that have custom destructors, copy/move constructors or copy/move assignment operators should deal exclusively with ownership (which follows from the Single Responsibility Principle). Other classes should not have custom destructors, copy/move constructors or copy/move assignment operators[1].
This rule also appears in the C++ Core Guidelines as C.20: If you can avoid defining default operations, do.
class rule_of_zero { std::string cppstring; public: // redundant, implicitly defined is better // rule_of_zero(const std::string& arg) : cppstring(arg) {} };
When a base class is intended for polymorphic use, its destructor may have to be declared public and virtual. This blocks implicit moves (and deprecates implicit copies), and so the special member functions have to be defined as = default[2].
class base_of_five_defaults { public: base_of_five_defaults(const base_of_five_defaults&) = default; base_of_five_defaults(base_of_five_defaults&&) = default; base_of_five_defaults& operator=(const base_of_five_defaults&) = default; base_of_five_defaults& operator=(base_of_five_defaults&&) = default; virtual ~base_of_five_defaults() = default; };
However, this makes the class prone to slicing, which is why polymorphic classes often define copy as = delete (see C.67: A polymorphic class should suppress public copy/move in C++ Core Guidelines), which leads to the following generic wording for the Rule of Five: