- Modern C++ Programming Cookbook
- Marius Bancila
- 1314字
- 2021-06-11 18:22:18
Defaulted and deleted functions
In C++, classes have special members (constructors, destructors, and assignment operators) that may be either implemented by default by the compiler or supplied by the developer. However, the rules for what can be default implemented are a bit complicated and can lead to problems. On the other hand, developers sometimes want to prevent objects from being copied, moved, or constructed in a particular way. This is possible by implementing different tricks using these special members. The C++11 standard has simplified many of these by allowing functions to be deleted or defaulted in the manner we will see in the next section.
Getting started
For this recipe, you need to be familiar with the following concepts:
- Special member functions (default constructor, destructor, copy constructor, move constructor, copy assignment operator, move assignment operator)
- The copyable concept (a class features a copy constructor and copy assignment operator making it possible to create copies)
- The moveable concept (a class features a move constructor and a move assignment operator making it possible to move objects)
With this in mind, let's learn how to define default and deleted special functions.
How to do it...
Use the following syntax to specify how functions should be handled:
- To default a function, use =default instead of the function body. Only special class member functions that have defaults can be defaulted:
struct foo { foo() = default; };
- To delete a function, use =delete instead of the function body. Any function, including non-member functions, can be deleted:
struct foo { foo(foo const &) = delete; }; void func(int) = delete;
Use defaulted and deleted functions to achieve various design goals, such as the following examples:
- To implement a class that is not copyable, and implicitly not movable, declare the copy constructor and the copy assignment operator as deleted:
class foo_not_copyable { public: foo_not_copyable() = default; foo_not_copyable(foo_not_copyable const &) = delete; foo_not_copyable& operator=(foo_not_copyable const&) = delete; };
- To implement a class that is not copyable, but is movable, declare the copy operations as deleted and explicitly implement the move operations (and provide any additional constructors that are needed):
class data_wrapper { Data* data; public: data_wrapper(Data* d = nullptr) : data(d) {} ~data_wrapper() { delete data; } data_wrapper(data_wrapper const&) = delete; data_wrapper& operator=(data_wrapper const &) = delete; data_wrapper(data_wrapper&& other) :data(std::move(other.data)) { other.data = nullptr; } data_wrapper& operator=(data_wrapper&& other) { if (this != std::addressof(other)) { delete data; data = std::move(other.data); other.data = nullptr; } return *this; } };
- To ensure a function is called only with objects of a specific type, and perhaps prevent type promotion, provide deleted overloads for the function (the following example with free functions can also be applied to any class member functions):
template <typename T> void run(T val) = delete; void run(long val) {} // can only be called with long integers
How it works...
A class has several special members that can be implemented, by default, by the compiler. These are the default constructor, copy constructor, move constructor, copy assignment, move assignment, and destructor (for a discussion on move semantics, refer to the Implementing move semantics recipe in Chapter 9, Robustness and Performance). If you don't implement them, then the compiler does it so that instances of a class can be created, moved, copied, and destructed. However, if you explicitly provide one or more of these special methods, then the compiler will not generate the others according to the following rules:
- If a user-defined constructor exists, the default constructor is not generated by default.
- If a user-defined virtual destructor exists, the default constructor is not generated by default.
- If a user-defined move constructor or move assignment operator exists, then the copy constructor and copy assignment operator are not generated by default.
- If a user-defined copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor exists, then the move constructor and move assignment operator are not generated by default.
- If a user-defined copy constructor or destructor exists, then the copy assignment operator is generated by default.
- If a user-defined copy assignment operator or destructor exists, then the copy constructor is generated by default.
Note that the last two rules in the preceding list are deprecated rules and may no longer be supported by your compiler.
Sometimes, developers need to provide empty implementations of these special members or hide them in order to prevent the instances of the class from being constructed in a specific manner. A typical example is a class that is not supposed to be copyable. The classical pattern for this is to provide a default constructor and hide the copy constructor and copy assignment operators. While this works, the explicitly defined default constructor ensures the class is no longer considered trivial and, therefore, a POD type. The modern alternative to this is using a deleted function, as shown in the preceding section.
When the compiler encounters =default in the definition of a function, it will provide the default implementation. The rules for special member functions mentioned earlier still apply. Functions can be declared =default outside the body of a class if and only if they are inlined:
class foo
{
public:
foo() = default;
inline foo& operator=(foo const &);
};
inline foo& foo::operator=(foo const &) = default;
The defaulted implementations have several benefits, including the following:
- Can be more efficient than the explicit ones.
- Non-defaulted implementations, even if they are empty, are considered non-trivial, and that affects the semantics of the type, which becomes non-trivial (and, therefore, non-POD).
- Helps the user not write explicit default implementations. For instance, if a user-defined move constructor is present, then the copy constructor and the copy assignment operator are not provided by default by the compiler. However, you can still default explicitly and ask the compiler to provide them so that you don't have to do it manually.
When the compiler encounters the =delete in the definition of a function, it will prevent the calling of the function. However, the function is still considered during overload resolution, and only if the deleted function is the best match does the compiler generate an error. For example, by giving the previously defined overloads for the run() function, only calls with long integers are possible. Calls with arguments of any other type, including int, for which an automatic type promotion to long exists, will determine a deleted overload to be considered the best match and therefore the compiler will generate an error:
run(42); // error, matches a deleted overload
run(42L); // OK, long integer arguments are allowed
Note that previously declared functions cannot be deleted as the =delete definition must be the first declaration in a translation unit:
void forward_declared_function();
// ...
void forward_declared_function() = delete; // error
The rule of thumb, also known as The Rule of Five, for class special member functions is that if you explicitly define any copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor, then you must either explicitly define or default all of them.
The user-defined destructor, copy-constructor, and copy assignment operator are necessary because objects are constructed from copies in various situations (like passing parameters to functions). If they are not user-defined, they are provided by the compiler, but their default implementation may be wrong. If the class manages resources, then the default implementation does a shallow copy, meaning that it copies the value of the handle of the resource (such as a pointer to an object) and not the resource itself. In such cases, a user-defined implementation must do a deep copy that copies the resource, not the handle to it. The presence of the move constructor and move assignment operator are desirable in this case because they represent a performance improvement. Lacking these two is not an error but a missed optimization opportunity.
See also
- Uniformly invoking anything callable to learn how to use std::invoke() to invoke any callable object with the provided arguments
- Learning Python Web Penetration Testing
- Advanced Splunk
- WebAssembly實戰
- 樂學Web編程:網站制作不神秘
- 匯編語言程序設計(第3版)
- 基于Swift語言的iOS App 商業實戰教程
- 青少年學Python(第1冊)
- Unity 2017 Mobile Game Development
- Java 從入門到項目實踐(超值版)
- Mastering HTML5 Forms
- 現代C:概念剖析和編程實踐
- Software Development on the SAP HANA Platform
- SAP Web Dynpro for ABAP開發技術詳解:基礎應用
- Python Penetration Testing Essentials
- Learning Ionic(Second Edition)