To my understand these semantics are used only with the copy constructor, moving constructor, copy assignment, moving assignment, and the destructor. Using = delete
is for prohibiting the use of one of the functions, and that = default
is used if you want to be explicit to the compiler on where to use the defaults for these functions.
What are the best practices when using these keywords while making a class? Or rather how do I keep mindful of these when developing a class?
For example, if I don't know whether I'll use one of these functions, is it better to prohibit it with delete
or allow it and use default
?
Good question.
Also important: Where to use = default
and = delete
.
I have somewhat controversial advice on this. It contradicts what we all learned (including myself) for C++98/03.
Start your class declaration with your data members:
class MyClass
{
std::unique_ptr<OtherClass> ptr_;
std::string name_;
std::vector<double> data_;
// ...
};
Then, as close as is practical, list all of the six special members that you want to explicitly declare, and in a predictable order (and don't list the ones you want the compiler to handle). The order I prefer is:
// this tells me the very most important things about this class.
// I like to see my copy members together
// I like to see my move members together
The reason for this order is:
For example:
class MyClass
{
std::unique_ptr<OtherClass> ptr_;
std::string name_;
std::vector<double> data_;
public:
MyClass() = default;
MyClass(const MyClass& other);
MyClass& operator=(const MyClass& other);
MyClass(MyClass&&) = default;
MyClass& operator=(MyClass&&) = default;
// Other constructors...
// Other public member functions
// friend functions
// friend types
// private member functions
// ...
};
Knowing the convention, one can quickly see, without having to examine the entire class declaration that ~MyClass()
is implicitly defaulted, and with the data members nearby, it is easy to see what that compiler-declared and supplied destructor does.
Next we can see that MyClass
has an explicitly defaulted default constructor, and with the data members declared nearby, it is easy to see what that compiler-supplied default constructor does. It is also easy to see why the default constructor has been explicitly declared: Because we need a user-defined copy constructor, and that would inhibit a compiler-supplied default constructor if not explicitly defaulted.
Next we see that there is a user-supplied copy constructor and copy assignment operator. Why? Well, with the data members nearby, it is easy to speculate that perhaps a deep-copy of the unique_ptr ptr_
is needed. We can't know that for sure of course without inspecting the definition of the copy members. But even without having those definitions handy, we are already pretty well informed.
With user-declared copy members, move members would be implicitly not declared if we did nothing. But here we easily see (because everything is predictably grouped and ordered at the top of the MyClass
declaration) that we have explicitly defaulted move members. And again, because the data members are nearby, we can immediately see what these compiler-supplied move members will do.
In summary, we don't yet have a clue exactly what MyClass
does and what role it will play in this program. However even lacking that knowledge, we already know a great deal about MyClass
.
We know MyClass
:
OtherClass
.ptr_
, empty name_
and data_
.That's a lot to know within 10 or so lines of code. And we didn't have to go hunting through hundreds of lines of code that I'm sure are needed for a proper implementation of MyClass
to learn all this: because it was all at the top and in a predictable order.
One might want to tweak this recipe say to place nested types prior to the data members so that the data members can be declared in terms of the nested types. However the spirit of this recommendation is to declare the private data members, and special members, both as close to the top as practical, and as close to each other as practical. This runs contrary to advice given in the past (probably even by myself), that private data members are an implementation detail, not important enough to be at the top of the class declaration.
But in hindsight (hindsight is always 20/20), private data members, even though being inaccessible to distant code (which is a good thing) do dictate and describe the fundamental behaviors of a type when any of its special members are compiler-supplied. And knowing what the special members of a class do, is one of the most important aspects of understanding any type.
Every type has answers to these questions, and it is best to get these questions & answers out of the way ASAP. Then you can more easily concentrate on what makes this type different from every other type.
Also, using =default
instead of a hand-rolled one keeps the POD nature of the class, as it is described here in detail: Default constructors and POD
You often see = default
when you are trying to maintain the rule of 5 to ensure the special member functions behave as you intend them to, and so that the reader of the class can see that you did consider how you wanted that function to behave.
You can use = delete
if you intent to make something non-copyable or non-movable, for example. Though I have also seen people delete
an inherited function if they do not want that specific derived class to have that function, though I'm not a huge fan of that since it tends to point towards poor architecture/design.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With