Statements and Expressions
Expressions with Booleans
Never use the constants true
or false
as operands to the operators
==
or !=
. It makes the expressions unnecessary long and hard to read.
// good
if (hasSomething && !isGood)
{
// ...
}
// bad
if (true == ((true == hasSomething) && (false == isGood)))
{
// ...
}
Trivial if-else
Just don’t do this, you are not paid per lines of code produced.
return isGood; // good
if (isGood) // bad
{
return true;
}
else
{
return false;
}
Avoid Magic Constants
Unnamed constants embedded in expressions are easily overlooked and often hard to understand.
constexpr int first_month = 1;
constexpr int last_month = 12;
for (int m = first_month; m <= last_month; ++m) // good, constants are named
{
std::cout << month[m] << '\n';
}
for (int m = 1; m <= 12; ++m) // bad, magic constants 1 and 12
{
std::cout << month[m] << '\n';
}
Null Pointer
Don’t use zero integer literal.
0
is a suboptimal way to represent a null pointer constant for multiple reasons. Firstly, it is
an integer literal, which is most commonly used to represent the numerical value zero rather than
an address. Its duality introduces cognitive overhead to source code readers.
int main()
{
// Unclear whether foo accepts an integer or address
something::foo(0);
}
Another problem lies in overload resolution. Overloading a function accepting int* with one accepting int might silently change the meaning of existing code:
// before overloading
void foo(int*); // (0)
// void foo(int); // (1)
int main()
{
foo(0); // Calls (0)
}
// after overloading
void foo(int*); // (0)
void foo(int); // (1)
int main()
{
foo(0); // Calls (1)
}
Don’t use the NULL macro.
The NULL
macro is defined as “an implementation-defined null pointer constant”. This gives
implementations the freedom to implement NULL as any zero integral literal (e.g. 0
, 0L
) or
as nullptr
. Similarly to the overload resolution situation described above, this definition can
cause problems when new overloads are added to an existing overload set.
Imagine adding a function accepting either std::nullptr_t
, long
, int
, or int*
to an
existing overload set that is being invoked with NULL
: the behavior is now hard to predict
and may vary depending on the standard library implementation being used.
Additionally, C and C++ code can interpret NULL differently which leads to incompatibilities.
Use nullptr!
Modern code should use nullptr
instead of 0
or NULL
to maximize readability and
prevent suprising overload resolution outcomes between pointer and integral types.
void foo(int*);
int main()
{
foo(nullptr);
}
See also:
Order of Evaluation
// undefined behaviour, i is evaluated more than once in an unsequenced manner
uint32_t a = i + b[++i];
// correct code
++i;
uint32_t a = i + b[i];
// undefined behaviour, the order of evaluation for function arguments is unspecified
func(a(), b());
// correct code
a_val = a();
b_val = b();
func(a_val, b_val);
For more information and examples see CERT rule EXP50.
Asserts
To control the behavior of asserts on ECUs, use the estd_assert
macro instead of the regular
assert
from the standard cassert header.
In opposite to a PC program where asserts are ignored in release builds, an estd_assert
will be present in release code causing an ECU reset and most likely a customer noticeable event.
We usually do not distinguish between debug and release builds.
Therefore, an estd_assert
shall only be used if continuing execution is impossible, e.g. because
memory got corrupted (out of bounds access through operator[]). Comment the usage with a
justification.
It might be acceptable during development to use asserts. In this case use estd_expect
instead.
estd_expect
behaves the same as estd_assert
, but it must be replaced before SOP with
proper error handling. The usage of estd_expect
is detected by our code analyzers and
presented in the module overview from Cijack.
Summary:
Prefer proper error handling over asserts
For temporary asserts, use
estd_expect
For production code asserts, use
estd_assert
and comment the usage with a justification
Forward Declarations
Use explicit forward declarations for:
Faster (incremental) builds
Better testable code
Prefer Using Over typedef for Defining Aliases
With using
, the new name comes first rather than being embedded somewhere in a declaration,
which improves readability. Additionally, using
can be used for template aliases, whereas
typedef
s can’t easily be templates.
using MyArray = ::estd::array<::some::other::Type, 199>; // good - new name comes first
typedef ::estd::array<::some::other::Type, 199> MyArray; // bad - new name at the end
template <typename T>
using ArrayOf5 = ::estd::array<T, 5>; // template alias - impossible with typedef
OS Specific Code
Try to avoid OS specific code. This makes your software hardly portable and difficult to test. Use
an abstraction layer, e.g. the async
library.
Critical Sections
...
{
ESR_UNUSED const interrupts::SuspendResumeAllInterruptsScopedLock lock;
// Critical section here
}
...
Buffers
If buffers are used in the code, ensure that they are protected against under- and overflow. This can be done implicitly by design or by range checks. If you decide not to implement such a check, e.g. due to performance reasons, document that behaviour.
A missing check might be unacceptable, e.g. in security or safety relevant code.
Delays
It is often hard to understand the values of delays. Explain the chosen values in a comment.
Retries
Document retries in the code or make it obvious by designing a clear API.