Good question.
Also important: Where to use = default and = delete .
I have some controversial tips on this. This is contrary to what we all learned (including me) for C ++ 98/03.
Start the class declaration with your data members:
class MyClass { std::unique_ptr<OtherClass> ptr_; std::string name_; std::vector<double> data_;
Then, as far as practicable, list all six special members that you want to explicitly declare, and in a predictable order (and do not list those that you want the compiler to process). The order I prefer:
- destructor
// this tells me the very most important things about this class. - default constructor
- copy constructor
// I like to see my copy members together Assignment Operator - move constructor
// I like to see my move members together Assignment Operator
The reason for this order is:
- No matter what special members you are by default, the reader is more likely to understand what they are doing by default if they know what the data members are.
- By listing special members in a consistent place near the top and in sequential order, the reader is more likely to quickly understand which special members are not explicitly declared & dashes; and therefore are either implicitly declared or do not exist at all.
- Typically, both copies of the copy (constructor and assignment) are similar. Either both will be an implicit default or remote, explicitly default, or remote or explicitly provided. It's nice to confirm this in two lines of code next to each other.
- As a rule, both movement elements (constructor and assignment) are similar ...
For instance:
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;
Knowing the convention, you can quickly see it without looking at the full class declaration, which ~MyClass() implicitly defaulted, and next to the data elements it is easy to see what this destructor, declared by the compiler and supplied, does.
Further, we see that MyClass has an explicitly default default constructor, and with the data members indicated nearby it is easy to see what this default constructor provided by the compiler does. It is also easy to understand why the default constructor was explicitly declared: since we need a custom copy constructor, and this will prevent the default constructor provided by the compiler if it is not explicitly set by default.
Further we see that there is a copy of the copy created by the user and a copy assignment operator. What for? Well, with the adjacent data, it's easy to assume that perhaps a deep copy of unique_ptr ptr_ . We cannot know this, of course, without checking the definitions of the members of the copy. But even without these definitions being convenient, we are already pretty well informed.
With user-declared copy members, the movement of elements will not be implicitly declared unless we do nothing. But here we easily see (because everything is predictably grouped and ordered at the top of the MyClass declaration) that we explicitly deform the movement elements. And again, since the data elements are nearby, we can immediately see what these elements related to the compiler will do.
So, we still do not know what exactly MyClass does and what role it will play in this program. However, even without this knowledge, we already know a lot about MyClass .
We know MyClass :
- Holds a uniquely owned pointer to some (possibly polymorphic)
OtherClass . - Holds a string serving as a name.
- Holds a bunch of doubles sharing like some data.
- Correctly destroy himself, not missing anything.
- Will be built by default with null
ptr_ , empty name_ and data_ . - It will copy itself, and not positively how it is, but there is a probable algorithm that we can easily check elsewhere.
- It will move efficiently (and correctly) by moving each of the three data items.
This is a lot to know within 10 or so lines of code. And we did not need to look for hundreds of lines of code, which, I am sure, are necessary for the correct implementation of MyClass in order to find out all this: because everything was at the top and in a predictable order.
You might want to customize this recipe to put nested types in front of data members so that data members can be declared in terms of nested types. However, the spirit of this recommendation is to declare private data members and special members as close to practical as possible and as close to each other as possible. This contradicts the recommendations given in the past (perhaps even to oneself) that private data members are implementation details that are not important enough to be at the top of the class declaration.
But in hindsight (hindsight is always 20/20), private data members, even being inaccessible to remote code (which is good), dictate and describe the fundamental behaviors of a type when any of its special members are supplied by the compiler. And knowing what special class members do is one of the most important aspects of understanding any type.
- Is it destructive?
- Is it constructive by default?
- Can I copy it?
- Can this be done?
- Does it have semantics of meanings or reference semantics?
Each type has answers to these questions, and it is best to answer these questions and answers. Then you can more easily focus on what makes this type different from any other type.