Skip to content
Tobias Wollgam edited this page Apr 22, 2024 · 9 revisions

Modern C++

Use good C++ style. No C strings (char*), atoi or other things you may have become familiar with when coding "C/C++".

We are using the C++17 standard but since the compiler may lack some of the features, we try to be careful not to use any new features not supported by recent versions of GCC, Clang and MSVC. The compiler support is actually very good but you will quickly run into trouble with the new library additions.

We do not use delete, delete[] or new[] and we avoid using "new". Instead, standard library containers and smart pointers are used when necessary.

We use references instead of pointers whenever possible. Resources are always managed by RAII (see http://www.hackcraft.net/raii/ ). We generally avoid inheritance (when not directly useful). Errors are always handled by throwing std::runtime_error (for errors in input data, etc) or std::logic_error (internal errors of Performous), or something derived from those.

Source code files

Preferred file extensions are .cc (C++ source), .hh (C++ header) and .ii (C++ include file). The last type is only used for code generation, and normally only source and header files are found. Usually source and header come as a matching pair but this is not a requirement.

Each header file begins with #pragma once and a possible Doxygen comment describing the file, followed by the minimal set of #includes needed. Notice that forward declarations can often be used to avoid including another header, and this is preferred to avoid circular dependencies and to cut down compile times.

Source code files begin by including the corresponding header file (if there is one), then any other Performous header files, then external libraries and finally stdlib headers. Within each group, alphabetical order should be maintained if possible as this helps keeping the list as short as possible. Notice that CMakeFiles.txt does not need to be modified when adding new source code files to the game folder as CMake will scan the folder for any .cc files. However, you may need to re-run cmake after adding or removing files.

Include files usually assume certain macros to be defined before they are included, and they call those macros to generate some useful code. Sometimes include files may call themselves recursively. In any case, care must be taken with macros and generally the include file should #undef the macro that it used, when it is finished with it.

No copyright declarations of any kind are used in source code files. Such information belongs to docs/License.txt, should it matter.

Use one header (or .hh/.cc pair) per struct/class.

Put the implementation into the .cc-file. Put template code beneath the class/struct-declaration.

Code comments

Write comments only where needed! Don't comment things that are obvious.

If something is difficult to read and understand or works unexpected think twice if this could be made simpler. Often writing a new function with a descriptive name is better than writing a comment.

Source code files should be documented with the following Doxygen comment structure (note: Doxygen comments use three slashes)

 /// @file
 /// Example file

Classes should be documented where they are defined. Functions should be documented in the class definition (especially public functions) or at function definition (.cc file) but not both. The shorter end-of-the-line version (///<) is preferred for simple functions and data members:

 /// Example class
 class Example {
 public:
     Example(): m_value() {}  ///< Constructor zero-initializes the value
     int getValue() const { return m_value; }  ///< Get the value
     void setValue(int value);  ///< Set the value. @throw std::logic_error if value is unacceptable
     /// This function does this and that
     /// @return The result of those
     int process();
 private:
     int m_value;  ///< The stored value
 };

Some Doxygen comments may seem trivial in code but you have to remember that in the documentation the implementation is not visible and those comments should be written with this in mind.

Whitespace and formatting

  • Indentation is one tab character per a pair of {}.
  • Long lines that are split over several use one indent.
  • Labels (public/private, case/default) are one level "higher" than they should (i.e. one less tab).
  • if-/else-blocks start with a { at the same line and continues with the next line.

Do not try to align anything with the next/previous line of code. Maintaining the alignment wastes time and it also easily breaks with Emacs (which cannot understand that it should not convert those spaces that you used for alignment into tab characters).

Deprecated in 2024:

In 2024 we decided to use only "normal" indents and drop the "small indents".

Indentation is one tab character per a pair of {}. Long lines that are split over several use "small indent" of two spaces. Labels (public/private, case/default) are one level "higher" than they should (i.e. one less tab). Do not try to align anything with the next/previous line of code. Maintaining the alignment wastes time and it also easily breaks with Emacs (which cannot understand that it should not convert those spaces that you used for alignment into tab characters).

Integer types and looping

In general, you should always use unsigned types rather than int. Only use signed types when you actually need negative values or when talking with an outside library. Using unsigned avoids sign extension bugs, signed comparison bugs and related compiler warnings. In some cases unsigned types also optimize better. Array and container index values are of type std::size_t (even if unsigned is often used).

The C++11 for-each loop should be used whenever possible:

 for (auto const& val: container) {
     // Do something with val
 }

Conditional statements

We prefer short and simple code. E.g. if (foo) bar(); is easiest to read and debug when it is on separate lines.

When you can, use "if" with return, break, continue and throw to handle conditional situations instead of using if-elses. This makes the code much easier to read and avoids deep indentation levels.

Sometimes you may see a sequence construct that is lesser known to C++ programmers (tryWith is a function returning bool):

 // Try first with someValue and if that fails, try fallbacks
 tryWith(someValue) || tryWith(fallbackValue) || tryWith(finalFallbackValue);

Switch statements are evil

Do not use switch-case for anything but enum values (and never provide default label in that case). Just use ifs that break execution (return, continue, break or throw) or if-else structures.

Rationale: The switch statement is very badly designed. There are no blocks around cases by default, so if you need to define variables, you need to add a block yourself (causing total two levels of indentation). It is also very easy to forget a break, causing incorrect and hard-to-debug errors that you will get no compile warnings for. Additionally, you won't be able to break out of a surrounding loop like you can with if-elses. The reason why enum values can and should still be handled with switch-case is that GCC gives a warning if you do not handle all possible values, therefore catching a class of hard-to-find bugs and out-weighting the other disadvantages of switch statements.

Clone this wiki locally