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:
- destructor
// this tells me the very most important things about this class.
- default constructor
- copy constructor
// I like to see my copy members together
- copy assignment operator
- move constructor
// I like to see my move members together
- move assignment operator
The reason for this order is:
- Whatever special members you default, the reader is more likely to understand what the defaults do if they know what the data members are.
- By listing the special members in a consistent place near the top, and in a consistent order, the reader is more likely to quickly realize which special members are not explicitly declared ‐ and thus either implicitly declared, or not do not exist at all.
- Typically both copy members (constructor and assignment) are similar. Either both will
be implicitly defaulted or deleted, explicitly defaulted or deleted, or explicitly supplied. It is nice to confirm this in two lines of code right next to each other.
- Typically both move members (constructor and assignment) are similar...
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
:
- Holds a uniquely owning pointer to some (probably polymorphic)
OtherClass
.
- Holds a string serving as a name.
- Holds a bunch of doubles severing as some kind of data.
- Will properly destruct itself without leaking anything.
- Will default construct itself with a null
ptr_
, empty name_
and data_
.
- Will copy itself, not positive exactly how, but there is a likely algorithm we can easily check elsewhere.
- Will efficiently (and correctly) move itself by moving each of the three data members.
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.
- Is it destructible?
- Is it default constructible?
- Is it copyable?
- Is it movable?
- Does it have value semantics or reference semantics?
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.