In the previous part, I’ve shown my guidelines for setting up compilation units. When writing simple application code with C++11, either classes or free-functions should be your main building blocks. Therefor, in this part, I will focus on what to look out for when writing class declarations.
While templates can be very useful, they do not scale well as the code base gets larger. Metaprogramming or other niche styles have their places, too, but I like to look at those as a means to create language extensions rather than principal implementation tools.
Avoid inline implementations
…especially in header files. It can be tempting to write classes solely in the header file. In fact, it has almost become a sign of quality for parts of C++ code to be header only. But this scales badly in most cases, and evolving such a code-base will result in a dramatic explosion of compile times. Always splitting classes into a declaration and definition acts as a first-level compile- firewall and dependency-breaker. Users of your class no longer need to worry about changes in the implementation of the member functions of that class. Note that those changes are often indirect: a change only affects a class that is used in the implementation of your class’ member functions. By splitting the declaration and definition, users of your class do not have to be recompiled.
But why stop at the compiler? The same argument holds for programmers. If you start to split interface and implementation on this level, you automatically provide ‘reader-firewalls’ as well. By just providing a clean header file, you are giving readers sort of a manual for your class. No need to look at the implementation at all, if the interface is well-defined.
Inline code definition is also the main reason against excessive use of templates. Yes, they grant a lot of flexibility, but you pay a hefty price which needs to be justified by an enormous reduction of complexity elsewhere. In general, templates are a bit too powerful for their own good, which is why they need extra moderation.
Always declare implicit functions
Implicitly declared functions seem comfortable, but they have a few implications that are hard to understand. First of, if an implicit function gets generated for your class, it will be generated as inline. This means that the implementation becomes a dependency to all users of your class. This can have very subtle effects such as this:
#include <vector> class Entry; class EntryManager { public: EntryManager(EntryGenerator& generator); int getEntryCount() const; std::string getIDForEntry(int index) const; private: std::vector<Entry> mData; };
On the surface, it looks like there should be no dependency (other than the name) on MyEntry when including this header. But there is!
The destructor is not declared so it will get generated – as inline. Because deletion of a vector requires the held type to be complete, any place that needs to be able to destruct a MyEntryManager also needs to know how to destruct MyEntry, which is not intended at all. Remember there’s a total of six functions that can be implicitly generated! Because of that, there are analogous problems for copy-construction, assignment, move-construction and move-assignment.
To avoid these problems, either delete the function explicitly in the header, default it in the implementation file, or actually implement it. You rarely need to do the latter, so I advise to default all the ones you need, and delete the rest:
#include <vector> class Entry; class EntryManager { public: EntryManager(EntryGenerator& generator); EntryManager(EntryManager const&)=delete; EntryManager& operator=(EntryManager const&)=delete; EntryManager(EntryManager&& rhs); EntryManager& operator=(EntryManager&& rhs); ~EntryManager(); int getEntryCount() const; std::string getIDForEntry(int index) const; private: std::vector<MyEntry> mData; };
And somewhere in the implementation file:
EntryManager::EntryManager(EntryManager&& rhs) = default; EntryManager::~EntryManager() = default; EntryManager& EntryManager::operator=(EntryManager&& rhs) = default;
This has another nice side effect because the vector-template gets instantiated into that object file and does not “bloat” all use-sites.
Exactly one public function and one private data section per class
..starting with the public section. This is where you address the next programmer that has to read your class. And it should be the only place for him to look.
I avoid private member functions because they cannot be tested easily and can add hidden compile-time dependencies to a project. Why should a user of your class recompile if you change an implementation detail? For small and trivial implementation helpers, the unnamed-namespace in the implementation file is a much better place. If those helpers become larger or more complex, it is a better idea to implement them in a collaborating class, which can be tested and reused.
Protected member functions split your interface to two parts, one exclusively for derived classes and one for everyone (including derived classes). This is very rarely needed, and in almost all of those cases, a separate interface will scale better (although it is slightly harder to implement).
Either an interface or an implementation
So far, I have left inheritance out of the picture and only talked about concrete classes. Inheritance is actually rarely needed, composition often suffices. But if it is needed, make sure that a class is either concrete and final (implementations), or has a complete and minimal set of pure-virtual member functions (interfaces). This will result in shallow hierarchies and easily understood interfaces. Remember that inheritance is not a tool for sharing code from the classes you implement, but for the code using those classes – i.e. where the Liskov Substition Principle holds.
Now it gets really easy to implement new classes in the hierarchy: Just implement all the functions in the interface. No more questioning whether to leave the default behaviour or override. You will also automatically tend towards clearer separation of components – things that need to be polymorphic move to the interface, other functionality merely uses it.
This pattern is useful even when polymorphy is not needed. Such small interfaces devoid of any implementation detail can act as another compiler firewall. Collaborators can work with just the interface and do not have to be recompiled when the implementation changes. Also, the interface can be implemented for mock or fake objects in testing.
Conclusion
This concludes the second part of the series. I originally intended it to be about how to write a whole class, but that would have been too much to digest for one post. I am well aware that some of these guidelines can stir quite the controversy in the C++ community. For example, declaring the implicit functions seems to be in conflict with the recently popular rule of zero. Scott Meyers had similar concerns, but does not quite touch the inline aspect.
For me personally, these guidelines have helped tremendously, especially when scaling to bigger code-bases. But as before, I am curious what others are thinking about this!
I think the example with std::vector and incomplete types is a bit misleading. Practically speaking the problem you mentioned does not exist, because with reasonable compilers/libraries the code shouldn’t compile (tested with GCC-5 and Clang-3.8). Theoretically, you are not allowed to use std::vector with incomplete types “at all” (i.e. the problem is not specific to destruction), although that might change in the future: http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4510.html
I guess I didn’t get that across right. That such code does not compile (or link, depending on the platform), is one of the problems. If you define the d-tor inline, you will “leak” Entry’s incompleteness to users of EntryManager. Therefore, users of EntryManager need to have Entry complete as well. If you define it out-of-line, however, you don’t need that. You can use EntryManager with Entry still being incomplete, therefore decoupling the classes a bit more (and possibly speeding up compilation times).
Note that it makes a difference whether special member functions are defaulted inline or separately (or implicitly). In your example, move constructor and move assignment are *not* declared as noexcept. Defaulting them inline would implicitly make them noexcept, and that can improve the performance significantly.
Thanks for the information! That’s very some subtle behaviour though. Do you know the reason for that?
If a special member function is not explicitly defaulted within the class definition, then the compiler can’t know that it is explicitly defaulted when the class is used. If the special member function is explicitly defaulted at first declaration, then it is just a convenience to omit certain parts of the declaration (e.g. constexpr). Of course it could have been specified differently, but I think there is no perfect solution.