This coding-standard is still work in development. It is however complete enough to give a first impression of what I expect from code that should become part of projects under my controll.
It is very important to understand that few things here are to be understood as absolute gospel. These are guidelines, not laws.
Naming Conventions
The naming-conventions here basically boil down to “Do as the standard-library does” and only deviates from that if there are very serious technical reasons or when it doesn’t provide any guidence.
- Classes, functions (including methods), variables, constants, aliases, namespaces and templates are all named in
lower_snake_case
.
- Private attributes are to be prefixed with “
m_
”. For example: m_size
.
- All macros are named in
ALL_CAPS
- Template-arguments are named in
PascalCase
- Concepts, once they are available, will be named
Snake_case_with_upper_first_letter
.
- All files are named in
lower_snake_case
and use on of the following file-extensions:
.cpp
for all source files that are directly compiled
.hpp
for headers
.tcc
may be used for files that are technically headers, but semantically implementation. For example if there is a header that declares a lot of templates without defining them, the definitions may be put into a file with this extension. These files must not be part of the public interface!
- Include-guards should in general be named like the files name converted to uppercase and all special-characters replaced with underscores. For example:
FOOBAR_HPP
For libraries and modules of large projects, the following rules apply in addition:
- All Macros must be prefixed with the name of the appropriate namespace converted to uppercase. For Example:
MYLIB_SOME_MACRO
instead of SOME_MACRO
.
- The above rule also applies for include-guards:
MYLIB_FOOBAR_HPP
The following names must never be used under any circumstances, since they are reserved for the implementation by the C++-standard:
- Any identifier that starts with an underscore followed by a capital letter.
- Any identifier that contains double-underscores. (
foo__bar
)
- Any identifier that starts with an underscore and is part of the global namespace.
(The last rules shouldn’t be necessary here, but they are violated extremely often, especially for include-guards, so let’s repeat them!)
Style Conventions
- In general opening braces do not belong on their own line, with the obvious exception of braces that exist solely for the purpose of creating a new scope. You may however use your own judgement to decide in corner-cases.
- Indent with tabs, align with spaces.
- the statements in an if/else-block or a loop-body must always be wrapped by braces.
- Very short code-blocks don’t have to be put on their own lines, if it helps readability
General Conventions
- Use the highest sane Warning-level. For GCC and clang that includes the following options:
-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion
Treat all warnings as errors.
- Do not use options like clangs
-Weverything
that activate all warnings, since quite a few of those are not useful in any way for the vast majority of projects. For instance: -Wc++98-compat
warns about the use of C++11-features.
- Write high-level code. It has often higher performance while being easier to understand.
Specific Issues
Ressource-Managment
- Manage all your Ressources with RAII!
- All state-holding classes should fall into one of these categories:
- Ressource-Managment-classes that only manage one kind of ressource and do noting besides that. (For example:
std::vector
, std::unique_ptr
, std::ofstream
, …)
- Bussines-Logic-classes that are used to implement the business-logic and that do not manage any ressources at all. In general these classes shouldn’t need customn special member-functions (assignment-operator, destructor, …).
- All code must be exception-safe, even if no exceptions are used. This must be accomplished by use of RAII, not by
try
-catch
-blocks.
- Bear in mind that memory is just one kind of ressource and that there are many others like filehandles, sockets, mutex-locks, threads, …
- Never lock and unlock mutexes manually, always use the appropriate mutex-managment classes like
std::unique_lock
and std::lock_guard
.
Sequence-Containers
- Do not use build-in arrays if you are not implementing an array-like container from scratch.
- Your default-container should be
std::vector
.
- If you are unsure whether you should use
std::vector
, just use std::vector
.
- If you want/need a container of compile-time fixed size, you may use
std::array
.
- Other containers like
std::list
and std::deque
may be used after careful consideration if some part of their interface is required. They may also be used as an optimization if measuring shows sufficient performance-advantages
Heap-allocation
- Prefer normal (stack-) variables, if those are sufficient. This should be over 95% of the time.
- If you really have to put a variable onto the free-store, use a
std::unique_ptr
and allocate it with std::make_unique
; never use new
here. This should be sufficient for over 90% of the remaining times.
- If that is still insufficient, you may use a
std::shared_ptr
. Those may be created with either std::make_shared
or, if you already have one of those, from a std::unique_ptr
.
- In the very rare event that this is still not enough, you may additionaly use
std::weak_ptr
.
- If you feel that this is still insufficient:
- If you are implementing fundamental, generic datastructures in their own class-templates, you may use raw-pointers in combination with allocators. Be careful!
- Otherwise redo your design for it is almost certainly to complicated and likely overly hard to understand the control-flow of it.
- A note on
new
: The allocating new
shouldn’t be directly used for basically anything these days, since stdlib-containers and smart-pointers with their make_*
-functions have taken over basically all legit use-cases. At this point it should be treated as at least as bad as goto: It still has a tiny amount of legit use-cases, but most people don’t run into those and should avoid it like the plague. (This paragraph does not talk about placement-new, which is an experts-feature that hasn’t seen a similar history of abuse and is still desperatly needed in quite a few situations.)
- A note on shared-pointers: With the arrival of C++11 a lot of new projects seem to have started using it for almost everything, similar to the way that pointers are used in Java. While this is an improvement over the previous use of raw-pointers to memory allocated with
new
, it is still a very awful trend that needs to stop. C++ works by far best if you just use normal stack-variables (even if those use the heap internally like std::vector
).
Constants
- If you don’t change a variable, make it const.
- If you have to use ugly trickery to make a variable const, don’t make it const!
- If a variable is used from several places, make sure that it is const in as many of them as possible.
- Contrary to popular opinion, mutability is not a problem, but shared mutability is a big one, even without the presence of multithreading.
Functions
- Try to keep the complexity in functions low and the body short.
- Use more of them, so that they can become smaller.
- Just because you don’t do something twice, doesn’t mean that it shouldn’t be in it’s own function!
- Use Lizard regularly. No function should ever have a cyclomatic complexity higher than 15. (It’ possible to write floating-point parsers in less!) Usually you shouldn’t need to leave the lower single-digits at all!
- Limit the number of loops in a function extremely: If you need more than one, seriously consider what could reasonably be moved to other functions. This is not a hard ban, but a very strong goal.
- Pass functions-arguments by const reference as default.
- If the type of the argument is just an empty tag-type, pass them by value instead.
- If the type of the argument is an integral datatype, a floating-point-type, a pointer or a thin wrapper around any of those (that is documented to be that), you may pass them by value as well.
- If the argument is templated and an iterator or a callable object, you may pass them by value to resemble stdlib-algorithms.
- If the function is a constructor and the argument is used to initialize a member, you may pass it by value and move it.
- If you need an rvalue you may either take the argument by value or by rvalue-reference.
- Avoid mutable references as arguments, except for operators and very few special-cases where they are required to blend in with common idioms.
- Never take arguments by const rvalue!
- Be aware that a template-parameter that is used for a rvalue-reference is actually a forwarding-reference!
Classes
- If a class has no state at all and is not used as some kind of type-tag or similar thing (policy-type for templates, meta-function, …), chances are, that whatever it models, it shouldn’t be a class.
- Don’t make classes virtual if there is no good reason for it. Especially don’t add a virtual destructor in that case.
- If you override a member-function of a base-class, annotate that with
override
.
- Don’t abuse classes as namespaces. Instead use, you guessed it, namespaces.
- Never declare more than one member-variable per line.
References
Freely use constant references for function-arguments that are only read.
Think before you use mutable references for function-arguments as they can be confusing. Simply returning is often a better alternative.
Think thrice before creating variables of reference types, as they can easily create lifetime-issues. If you are not 100% sure that it is safe or it is safe but only because of lifetime-extension, do not do it!
void f1(const std::vector<int> vec) {
assert(!vec.empty());
auto& c = vec.front(); // acceptable
// note the absence of changes to vec (here: enforced by const)
use(c, c); // this would be unpleasant without ref
}
void f2(std::vector<int> vec) {
assert(!vec.empty());
auto& y = vec.front();
vec.push_back(3);
f(y, y); // very bad, this is UB!!
}
void f3(const std::vector<std::string> vec) {
for(auto& x: vec) { // this is fine, as would be `auto&&` or `const auto&`
use(x);
}
}
Think five times before creating member-references in classes/structs. They are almost always a very bad idea and will mess with assignment-operators and the like in potentially surprising ways. In the vast majority of cases a naked (non-owning) pointer is a much better alternative here. Even the use in the tuple created by std::tie
is considered questionable by very competent people. If you still want to do this, write an extensive explanation why it is needed in the place in question and why it is safe.
Macros
Algorithms
- Prefer the use of stdlib-algorithms to self-written loops and conditionals, since those are in general correct, more semantic due to having names and experienced programmers may already know them.
- For that purpose: Make sure that you understand iterators and use them liberally. Be aware of the existence of iterators like the stream-iterators or the insertion-iterators (see for instance
std::back_inserter
)
- A huge lot of things can be done with stdlib-algorithms. Make sure that you know them and don’t forget that not all of them are in the
<algorithm>
-header, but also some in <numeric>
.
- Do not use algorithms from C, if their is also a C++-version. Those are often slower, more error-prone uglier to use and less general. (
qsort
→ std::sort
, memcpy
→ std::copy
(yes, std::copy
is just as fast, since it calls memmov if possible in both libc++ and libstdc++), …)
Streamoperations
- Do not use
std::endl
. Contrary to very popular opinion the difference compared to '\n'
is not, that only std::endl
is a portable newline (both are!), but that std::endl
also flushes the buffer, which is rarely ever needed and most of the time just premature pessimization). If you really want to flush the buffer, just follow the newline with a std::flush
so that everyone know that this is an explicit decission: stream << "Hello World\n" << std::flush;
- Do not use
std::fstream::open
and std::fstream::close
. Instead just use the constructor and the destructor. (If you really need to open late or close early for some reason, those are fine, but usually that also means that your design is suboptimal.)
- Be aware that C++11 made streams movable, so use that instead of adding another (smart-)pointer-indirection.