Version
- Version
-
v0.1.0
- Status
-
Draft
- Date
-
2025-05-30
This guideline is created according to the "Guidelines Standard" v0.1.0.
Overview
Best Practices to organize and structure code in cpp projects.
Goals
To have well-maintainable software projects and to facilitate collaboration with multiple people, best practices will be collected here that should be implemented in all C++ projects.
Requirements
Modularization
Create abstract classes as interfaces
Wherever possible, Abstract classes SHOULD be created as interfaces.
These abstract classes MUST only include headers of further abstract classes or headers defined in the C++ standard library (i.e., those that come with the compiler) in their headers. All other headers (e.g., Arduino headers, headers from external libraries, or headers from derived classes) are explicitly not permitted.
The name of an abstract class MUST start with Abstract
(i.e., follow
the schema Abstract<name>
).
Abstract classes SHOULD contain a virtual default destructor.
Abstract classes MUST consist only of a header file. It is not allowed
for them to also have a .cpp
file.
Abstract classes MUST NOT define private methods.
Abstract classes MUST NOT define attributes.
Abstract classes MUST define only public
functions. These MUST be
either virtual or inline (i.e., defined directly with a function body).
Abstract classes MUST NOT define a constructor.
In the header file, additional classes or data structures CAN be defined, but only if they are either abstract classes themselves or simple data structures (POD).
Correct | Incorrect |
---|---|
AbstractLightShowController.h
|
AbstractLightShowController.h
AbstractLightShowController.cpp
|
Reasoning
This strict use of abstract interfaces encourages stronger modularization of the software. This, in turn, promotes information hiding, which in turn fosters low coupling and strong cohesion (in other words, you don’t have spaghetti code).
This results in several benefits: The software becomes
-
easier to test (because you can easily derive test environments from the abstract interfaces)
-
easier to port (the abstract interfaces themselves are compilable on any system. Potential hardware-specific derivations are automatically encapsulated and can therefore be easily exchanged)
-
more flexible (you can easily create an experimental new implementation of an abstract interface and try it out. Since it is not connected to everything and is easily exchangeable, the risk of trying it out is significantly lower)
-
easier to understand (the strong modularization encourages taking a high-level view at every level. This makes every layer easy to understand on its own).
Derived classes should inherit from abstract classes
In the main
or setup
function, you will of course need concrete
implementations. The classes used there SHOULD inherit from abstract
classes.
Reasoning
The reasons are the same as for abstract classes themselves. Only by deriving classes from abstract classes wherever possible do the benefits of abstract classes become available.
Derived classes should not have an init function
Derived classes SHOULD NOT have an init
function; instead, the class
SHOULD be initialized directly in the constructor.
Reasoning
If you have a separate init
function, there is a difference in
calling the other functions of a class, depending on whether the class
has already been initialized or not. To avoid errors, you must catch
whether the class has already been initialized or not in each of these
functions. This is error-prone and also requires constant runtime
computation for the check.
If you perform the initialization in the constructor, an uninitialized state cannot occur when using the class.
Only include necessary headers
In all headers, only the absolutely necessary headers MUST be included. All headers that are not required for compiling the header MUST NOT be included.
This does not apply to headers that are implicitly included by other headers. In these cases, it is good to include them explicitly if the header is needed for compilation to make dependencies explicit.
If a header is only needed for the implementation but not in the header,
it MUST be moved to the source file (*.cpp
).
Reasoning
Dependencies can quickly arise unintentionally because you use a function/class/etc. from a header. This makes it difficult to restructure the source code later because you suddenly have more dependencies than expected. By only including the absolutely necessary headers, you minimize dependencies.
In derived classes, everything except the interface and constructor should be declared private
In derived classes, all functions and attributes except the derived
interface and the constructor SHOULD be declared private
.
In rare cases, it may happen that a function sensibly extends the derived interface without justifying its own abstract interface. In this case, you should make sure that the using classes and functions continue to use the abstract interface. If they become dependent on the extra function, it is a sign that you should extend the abstract interface or create a new abstract interface.
Reasoning
Dependencies can be reduced by declaring as much as possible as
private
. This encourages using the abstract interface and (if you
reach limits) improving it, rather than building a solution that is
difficult to port to new environments and difficult to restructure.
Getter and setter functions should only be defined when necessary
If you define getter or setter functions, there SHOULD be a concrete need for them in a using class or function.
You should rather consider whether you cannot use a function with a specific action or pass a whole structure instead.
Reasoning
The actual use cases usually define an action rather than setting a single value. Getter and setter functions often require a larger context to validate sensibly. Therefore, they are often a sign that implementation details have not been abstracted sufficiently.
Since they allow many more combinations than concrete functions with an action, they enable misuse of the API much more easily.
Data structures should be passed by value, not by reference
If you use data structures in a class and they can be queried or set, these MUST be passed by value and MUST NOT be passed by reference.
Reasoning
If you return an internal structure as a reference, the calling function or class gets access to the internal implementation details of the respective class and can manipulate them without using the API. This creates many invisible dependencies and makes it impossible to validate manipulations.
Structures and enums should be defined in abstract classes
If an abstract class uses data structures and/or enums that belong to the context of the abstract class [1], these SHOULD be defined in the class declaration.
Structures or enums in the context of an abstract class SHOULD NOT be defined directly in the global namespace. This also applies if you prefix them.
Correct | Incorrect |
---|---|
AbstractLightShowController.h
ConcreteLightShowController.h
ConcreteLightShowController.cpp
|
AbstractLightShowController.h
|
Reasoning
Defining data types within the class declaration helps avoid collisions in the namespaces, making compilation easier. Additionally, the names are shorter, which contributes to better readability and understandability of the source code.
Don’t use global variables
The code MUST NOT define nor use any global variables.
An exception to this is in the file where the main
or setup
function
is defined. Here a global variable MAY be defined and used. However all
other code defined in other files MUST NOT use those global variables.
Reasoning
The usage of global variables creates invisible coupling between otherwise unconnected parts of the code. The more it is used the more unmaintainable becomes a codebase.
General
Use UTF-8 encoding
In the source code, UTF-8 MUST be used as the encoding. Editors and IDEs must be configured accordingly.
Reasoning
In some cases, we need special characters in the source code (e.g., for i18n strings). In this case, Unicode is the only standardized way to cover all current and future languages.
UTF-8 is a very widely used encoding that is supported on all modern systems and is close to the normal ASCII code in terms of space consumption.
Use spaces instead of tabs
In the source code, spaces MUST be used for formatting instead of tabs. Editors and IDEs must be configured accordingly.
Reasoning
If you use tabs, the formatting can be completely messed up on different systems, since the tab width can vary on different systems. Although the tab width can be configured in editors, the source code can be very difficult to read in the default setting. With spaces, the original formatting is preserved when reading, even if the corresponding editor or viewer has a different tab width configured.