SpaceDot Coding Guidelines - PeakSat/obc-software GitHub Wiki
Author: Nikolaos Strimpas
These are the rules that will help us write software of consistent quality. Treat these rules as complementary to the C++ core guidelines. By no means these rules are written in stone and objections by OBSW members will be taken into account. It is recommended that the developer has read the CERT (for C and C++) and MISRA standards (can be found on the private cloud storage of the team). This document is also complementary to the formal technical documents of each project (DDJF_OBSW for AcubeSAT, SDR and MAIVP for PeakSAT).
This is not an educational medium and should be not treated in such way. The reader of this document is expected to know the basics of programming, C/C++, systems programming and embedded programming. This document contains some concrete rules, the use of which can help with homogeinity and quality of code.
For the current missions that SpaceDot is working on, the following languages are used:
- AcubeSAT:
- Assembly: One can read the ARM reference manual for Cortex M7 and for Cortex A9.
- C: The C11 standard is used (ISO/IEC 9899:2011). Some language extensions are permitted (mostly GNU ones). C is the side language for the two projects and is used after careful review by the OBSW team.
- C++: The C++17 standard is used (ISO/IEC 14882:2017). Some language extensions are permitted (mostly GNU ones). C++ is the main language for the two projects and most probably the one you will be writing in. C++ is also used for FPGA programming as an HLS (High Level Synthesis) language. There, a subset of C++14 is used and the following rules do not apply completely due to other limitations. If you are writing HLS code, please consult an FPGA engineer in the team.
- Other languages are also used (MATLAB, Octave, Python, Julia), but not for software that is meant to run on the satellite. The handbook can be extended to accomodate for those languages.
- PeakSAT:
- Assembly: One can read the ARM reference manual for Cortex M7.
- C: The C11 standard is used (ISO/IEC 9899:2011). Some language extensions are permitted (mostly GNU ones). C is the side language for the two projects and is used after careful review by the OBSW team.
- C++: The C++17 standard is used (ISO/IEC 14882:2017). Some language extensions are permitted (mostly GNU ones). C++ is the main language for the two projects and most probably the one you will be writing in.
- Other, higher level, languages are also used (Python), but not for software that is meant to run on the satellite. The handbook can be extended to accomodate for those languages.
Note that for future missions, this section shall be updated to report the language versions used in those. In case a more modern standard is used (for example by the time of writing, C++26, C2y/C3a), rules shall be added to accomodate for new features and practices (for example std::array
entering the freestanding library without the methods that throw or operator overloading in C). This also applies in case other languages are used, for example Rust or Ada/SPARK.
-
Git commit message shall not exceed 72 characters on each line.
-
Git commit messages shall use imperative tone.
Example
[Math]: Implement absolute value for floating point type
This implementation is exclusive to the IEEE 754 format and relies on
the fact that the MSB represents the sign of the number. In more
detail, the implementation just sets the MSB to 0 to ensure that the
result is always positive (in the IEEE 754 sense).
There are no precision reports, since the function relies on bitwise
math and not in floating point calculations. Floating point exceptions
are never raised for the same reason.
The implementation has passed the tests locally, with no regressions,
at least on the local setup that was used (FreeBSD 13.2
x86_64-unknown-freebsd clang).
-
Use spaces, not tabs. Tabs shall only appear in files that require them, like Makefiles.
-
The indent size is 4 spaces.
Right
int main() {
return 0;
}
Wrong
int main() {
return 0;
}
- The contents of namespaces shall be indented
Right
// Document.hpp
namespace Foo {
class Bar {
Bar();
...
};
namespace Baz {
class Toto {
Toto();
...
};
} // namespace Baz
} // namespace Foo
// Document.cpp
namespace Foo {
Bar::Bar() {
...
}
namespace Baz {
Toto::Toto() {
...
}
} // namespace Baz
} // namespace Foo
Wrong
// Document.hpp
namespace Foo {
class Bar {
Bar();
...
};
namespace Baz {
class Toto {
Toto();
...
};
} // namespace Baz
} // namespace Foo
// Document.cpp
namespace Foo {
Bar::Bar() {
...
}
namespace Baz {
Toto::Toto() {
...
}
} // namespace Baz
} // namespace Foo
- Case labels shall be indented. The case statement shall also be indented
Right
switch (toto) {
case tototiti:
case tototata:
doSomething();
i++;
break;
default:
doSomethingElse();
i--;
}
Wrong
switch (toto) {
case tototiti:
case tototata:
doSomething();
i++;
break;
default:
doSomethingElse();
i--;
}
- Boolean expressions at the same nesting level that span multiple lines shall have their operators on the left side of the line instead of the right side.
Right
return context.getSize() == correctSize
&& context.getMessage() == correctMessage
&& (context.getMessage().at(0) == 'A' || context.getMessage().at(0) == 'X');
Wrong
return context.getSize() == correctSize &&
context.getMessage() == correctMessage &&
(context.getMessage().at(0) == 'A' || context.getMessage().at(0) == 'X');
- When using the ternary conditional operator, and the expressions span multiple lines, the operator parts shall be on the left side of the line instead of the right side.
Right
struct Point {
float x;
float y;
};
Point point = someFunctionThatReturnsAPoint();
Point transformedPointOrNaN = isPointInsideBoundsOfTransform(point)
? applySaidTransformToThePointAndNormaliseNorm(point)
: NaNPoint;
Wrong
struct Point {
float x;
float y;
};
Point point = someFunctionThatReturnsAPoint();
Point transformedPointOrNaN = isPointInsideBoundsOfTransform(point) ?
applySaidTransformToThePointAndNormaliseNorm(point) :
NaNPoint;
- Do not place spaces around unary operators.
Right
i++;
j--;
++k;
--l;
+x;
-y;
Wrong
i ++;
j --;
++ k;
-- l;
+ x;
- y;
- Do place spaces around binary and ternary operators.
Right
y = a * b + c;
f(a, b);
r = q | p;
return condition ? 1 : -1;
Wrong
y=a*b+c;
f(a,b);
r = q|p;
return condition ? 1:-1;
- Do place spaces around the colon in a range-based for loop. Right
vector<Service, 30> serviceChain;
for (const auto& service : serviceChain) {
registerService(service);
}
Wrong
vector<Service, 30> serviceChain;
for (const auto& service: serviceChain) {
registerService(service);
}
- Do not place spaces before comma and semicolon.
Right
for (int i = 0; i < 5; i++) {
f(context, i);
}
f(context, [](int b, int c) { return b * b + c; });
Wrong
for (int i = 0 ; i < 5 ; i++) {
f(context , i);
}
f(context , [](int b , int c) { return b * b + c; }) ;
- Place spaces between control statements and their conditions.
Right
if (condition) {
doSomething();
}
switch (condition) {
case oneCondition:
doSomething();
case anotherCondition:
doSomethingElse();
default:
doABarrelRoll();
}
Wrong
if(condition) {
doSomething();
}
switch(condition) {
case oneCondition:
doSomething();
case anotherCondition:
doSomethingElse();
default:
doABarrelRoll();
}
- Place spaces between loop statements and their conditions.
Right
for (int i = 0; i < 25; i++) {
registerWithIndex(i);
}
while (x > maximumValue) {
x -= range;
}
StreamingData<40> currentValue;
do {
currentValue = inp.read();
use(currentValue);
} while (not currentValue.last)
Wrong
for(int i = 0; i < 25; i++) {
registerWithIndex(i);
}
while(x > maximumValue) {
x -= range;
}
StreamingData<40> currentValue;
do {
currentValue = inp.read();
use(currentValue);
} while(not currentValue.last)
- Do not place spaces between a function and its parentheses, or between a parenthesis and its content.
Right
f(a, b);
Wrong
f (a, b);
f( a, b );
- Do not place spaces between a function-like macro and its parentheses, or between a parenthesis and its content.
Right
#define SQUARE(x) ((x) * (x))
...
auto c = SQUARE(i);
Wrong
#define SQUARE(x) ((x) * (x))
...
auto c = SQUARE (i);
- Do not place spaces between square brackets and parentheses of a lambda function but do place a space before braces.
Right
[](int x) { return x; }
[this] { return member; }
Wrong
[] (int x) { return x; }
[this]{ return member; }
- When initializing an object, place a space before the leading brace as well as between the braces and their content.
Right
Foo foo { bar };
Wrong
Foo foo{ bar };
Foo foo {bar};
- Each statement shall get its own line.
Right
x++;
i += 5;
if (errno == ERANGE) {
performOutOfRangeReport();
}
Wrong
x++; i += 5;
if (errno == ERANGE) performOutOfRangeReport();
- Declarations are statements and therefore every declaration shall get its own line
Right
int x;
int y;
int z;
unsigned int m = 5U;
unsigned int n = 8U;
Wrong
int x, y, z;
unsigned int m = 5U, n = 8U;
- Chained
=
assignments shall be broken up into multiple statements.
Right
leftRange = totalRange / 4;
rightRange = totalRange / 4;
Wrong
leftRange = rightRange = totalRange / 4;
- An
else
statement shall go on the same line as a preceding close brace. Same goes forelse if
statements
Rigth
if (condition) {
...
} else {
...
}
if (condition) {
...
} else if (anotherCondition) {
...
} else {
...
}
- Place the open brace on the line preceding the code block; place the close brace on its own line. Place a space between the identifier and the opening brace.
Right
class MyClass {
...
};
struct MyStruct {
...
};
union MyUnion {
...
};
enum MyEnum {
...
};
namespace Core {
...
}
void foo() {
...
}
for (int i = 0; i < 5; i++) {
...
}
if (condition) {
...
}
switch (condition) {
...
}
while (condition) {
...
}
do {
...
} while(condition);
Wrong
class MyClass
{
...
};
struct MyStruct
{
...
};
union MyUnion
{
...
};
enum MyEnum
{
...
};
namespace Core
{
...
}
void foo()
{
...
}
for (int i = 0; i < 5; i++)
{
...
}
if (condition)
{
...
}
switch (condition)
{
...
}
while (condition)
{
...
}
do
{
...
} while(condition);
Also right
#include <cstdio>
void func(int x, int y) {
for (int i = x; i < 50; i++) {
if (x < 0) {
std::puts("Still under zero");
}
}
int j = y;
while (j < y) {
std::puts("Still under j");
}
}
Also wrong
#include <cstdio>
void func(int x, int y){
for (int i = x; i < 50; i++){
if (x < 0){
std::puts("Still under zero");
}
}
int j = y;
while (j < y){
std::puts("Still under j");
}
}
- Control clauses without a body shall use empty braces (though you should avoid them when writing in C++).
Right
for (; current != nullptr; current->next) { }
Wrong
for (; current != nullptr; current->next);
-
Use
nullptr
andnullptr_t
when working in C++. UseNULL
and never assume its type in C, unless you are working in C23, wherenullptr
andnullptr_t
are available.
Right
// Document.cpp
int* iptr = nullptr;
long long* llptr = nullptr;
long double* ldptr = nullptr;
void* (*fnptr)(void*) = nullptr;
// Document.c
#include <stddef.h>
int* iptr = NULL;
long long* llptr = NULL;
long double* ldptr = NULL;
void* (*fnptr)(void*) = NULL;
// Document.c in case you are writing in C23
int* iptr = nullptr;
long long* llptr = nullptr;
long double* ldptr = nullptr;
void* (*fnptr)(void*) = nullptr;
Wrong
// Document.cpp
int* iptr = 0;
long long* llptr = NULL;
long double* ldptr = (void*)0;
void* (*fnptr)(void*) = 0L;
// Document.c
int* iptr = 0;
long long* llptr = NULL;
long double* ldptr = (void*)0;
void* (*fnptr)(void*) = 0L;
// Document.c in case you are writing in C23
int* iptr = 0;
long long* llptr = NULL;
long double* ldptr = (void*)0;
void* (*fnptr)(void*) = 0L;
- When checking for null pointers use explicit equality.
Right
// Document.cpp
int* iptr = something();
if (iptr == nullptr) {
...
}
// Document.c
int* iptr = something();
if (iptr == NULL) {
...
}
Wrong
// Document.cpp
int* iptr = something();
if (!iptr) {
...
}
// Document.c
int* iptr = something();
if (!iptr) {
...
}
- Filenames shall use the
PascalCase
convention. Use.cpp
and.hpp
for file extensions in C++. When writing in C, use.c
and.h
for file extensions.
Right: GMSKTranscoder.hpp
, GMSKTranscoder.cpp
, ErrorHandling.h
, ErrorHandling.c
Wrong: GMSKTranscoder.h++
, GMSKTranscoder.cc
, ErrorHandling.hi
, ErrorHandling.c99
- Namespaces shall be written in
PascalCase
.
Right
namespace FooBaz {
namespace FooBar {
...
} // namespace FooBaz
...
} // namespace FooBar
Wrong
namespace fooBaz {
namespace foo_bar {
...
}
...
}
- For class, struct, union, enum, enum class and every other type name, use
PascalCase
. Also, never use the_t
suffix when naming your own types, since it may cause namespace collision with the C standard library (In POSIX systems names with the_t
suffix are reserved identifiers)
Right
struct FooBar {
...
};
class BarBaz {
...
};
union BazFoo {
...
};
enum TagWeak {
...
};
enum class TagStrong {
...
};
const volatile int x;
using RemoveQualsInt = typename std::remove_cv<decltype(x)>::type;
using Complex = std::complex<double>;
Wrong
struct foo_bar {
...
};
struct barbaz {
...
};
union bazFoo {
...
};
enum TAG_WEAK {
...
};
enum class Tag_Strong {
...
};
const volatile int x;
using remove_quals_int_t = typename std::remove_cv<decltype(x)>::type;
using complex = std::complex<double>; // You are asking for problems here
- Use
PascalCase
for constants other than enum variants.
Right
constexpr unsigned int NumberOfStudents = 24U;
using namespace std::complex_literals;
constexpr std::complex<double> TargetEigenValue = 1.0 + 0.707i;
Wrong
constexpr unsigned int number_of_students = 24U;
using namespace std::complex_literals;
constexpr std::complex<double> TARGET_EIGEN_VALUE = 1.0 + 0.707i;
- Use
camelCase
for variables and function names.
Right
class Foo {
public:
Foo() = default;
Foo(int x)
: num(x) { }
int getNum() const {
return num;
}
void incrementNum() {
num++;
}
private:
int num; // Also written in camelCase
};
int incrementTheNumAndGetItsValueWithWeight(Foo& self, int weightCoeff) {
self.incrementNum();
return self.getNum() * weightCoeff;
}
Wrong
class Foo {
public:
Foo() = default;
Foo(int x)
: Num(x) { }
int GetNum() const {
return Num;
}
void increment_num() {
Num++;
}
private:
int Num;
};
int Increment_The_Num_And_Get_Its_Value_With_Weight(Foo& self, int wgCff) {
self.increment_num();
return self.GetNum() * wgCff;
}
- Use
CAPITAL_CASE
for enum variants.
Right
enum class Woodwinds : int {
RECORDER,
SAXOPHONE,
CLARINET,
OBOE,
FAGGOTO,
FLUTE,
PICCOLO_FLUTE, // Actually a member of the flute family but I just wanted to show the underscore
};
Wrong
enum class Woodwinds : int {
Recorder,
Saxophone,
Clarinet,
Oboe,
Faggoto,
Flute,
PiccoloFlute,
};
- Use
CAPITAL_CASE
for macros. For function like macros, usecamelCase
for its parameters.
Right
#define SQUARE(num) ((num) * (num))
#define BTN_ID XPAR_AXI_GPIO_BUTTONS_DEVICE_ID
#define FIVE_OF_TYPE(type) ((type)5)
#define FOUR 4
Wrong
#define square(num) ((num) * (num))
#define btnID XPAR_AXI_GPIO_BUTTONS_DEVICE_ID
#define five_of_type(type) ((type)5)
#define Four 4 // People will think this is a constexpr constant and will attempt to take its address
-
Never use identifiers with underscore or double underscore as prefix (also known as
__uglified
,_Uglified
or_uglified
identifiers). These are reserved for the language implementers.
- Prefer using enums to macros.
- Prefer using
enum class
to plainenum
to avoid namespace collisions and stay true to the strong typing rule. If your sole purpose is to use an enumeration to do math with integers, you may use plainenum
, though make sure to use the narrowest scope possible. This is a C++ only rule. - Always use a trailing comma in enums.
Right
enum class HistoricPeriodsOfClassicalMusic : int {
BAROQUE,
CLASSICAL,
ROMANTIC,
CONTEMPORARY,
MODERN,
};
Wrong
enum class HistoricPeriodsOfClassicalMusic : int {
BAROQUE,
CLASSICAL,
ROMANTIC,
CONTEMPORARY,
MODERN
};
- Unless it is only serving the purpose of a tag for a few variants (like in the examples above), an enum declaration shall include its underlying type. This rule applies to C++ code and C23 if you are writing in it. If you are writing in C17 or older, which is the case for AcubeSAT and PeakSAT, just try to define enums that are inside the range of type
int
(that is [INT_MIN
,INT_MAX
]) and cast them when needed. If you have to exceed these limits, use macros, or, if you really want an enum, consult your compiler vendor to find out how enums are defined by the implementation (each compiler uses its own rules prior to C23).
Right
#include <cstdint>
enum class ControlReg : std::uint8_t {
READ = std::uint8_t{ 0b1111'1111 },
WRITE = std::uint8_t{ 0b1111'1110 },
CLEAR = std::uint8_t{ b1111'1101 },
INVALID_STATE = std::uint8_t{ 0b0000'1111 },
};
Wrong
enum class ControlReg {
READ = 0b1111'1111,
WRITE = 0b1111'1110,
CLEAR = 0b1111'1101,
INVALID_STATE = 0b0000'1111,
};
- When writing in C++ (or C23 and above), use the standard
bool
type and the standardtrue
andfalse
literals. When writing in C (C99 and above, prior to C23), import the<stdbool.h>
header from the standard library (available even on freestanding platforms, which are really common in embedded), and use thebool
macro (which expands to_Bool
, so_Bool
is actually the boolean type) andtrue
andfalse
macros. Be careful when including this header in headers that are going to be consumed by C++ code. There is no danger of the header not existing, butbool
,true
,false
may still be macros to something other than the C++ versions, if the C standard library vendor, or the C++ standard library vendor didn't care enough to make a special case for those and that can complicate things. Unfortunately, there is not really good advice on this subject, so ping some senior OBSW members if you find yourself facing issues with this. Most probably you won't, since those who implement the language take care of these things, so don't worry, just be careful. You could add astatic_assert(std::is_integral_v<bool>, "Type bool is not really C++ bool");
in your code, but I don't think that will really save you in these cases.
Right
// Document.cpp or Document.c if you are using C23
bool sunsetIsBeautifulFromTheSpaceDotLab = true;
bool sunsetIsVisibleWhenItIsHeavilyRaining = false;
// Document.c prior to C23
#include <stdbool.h>
bool sunsetIsBeautifulFromTheSpaceDotLab = true;
bool sunsetIsVisibleWhenItIsHeavilyRaining = false;
Wrong
// Document.cpp or Document.c if you are using C23
bool sunsetIsBeautifulFromTheSpaceDotLab = 1;
bool sunsetIsVisibleWhenItIsHeavilyRaining = 0;
// Document.c prior to C23
_Bool sunsetIsBeautifulFromTheSpaceDotLab = 1U;
int sunsetIsVisibleWhenItIsHeavilyRaining = 0U;
- When compared, a boolean expression should not be compared to neither true, or false.
Right
bool cond = something();
if (cond) {
...
}
Wrong
bool cond = something();
if (cond == true) {
...
}
- You can use whichever of the two spellings for logical operators you want, as long as you stay consistent with it. For C code, import the
<iso646.h>
header first (available even on freestanding platforms), since those alternative spellings are implemented as macros. Forif
statements in which the condition is something along the lines ofnot condition
, prefer the alternative spelling of the logicalNOT
operator for better readability.
Prefer this
if (not someCondition()) {
...
}
to this
if (!someCondition()) {
...
}
For both languages, the standard integer types are bool
/_Bool
, char
, char8_t
(C++20 and later, just a typedef in C23), char16_t
(C++11 and later, just a typedef in C11), char32_t
(C++11, and later, just a typedef in C11), wchar_t
(just a typedef in C) signed char
, unsigned char
, short
, unsigned short
, int
, unsigned int
, long
, unsigned long
, long long
, unsigned long long
. For rules on bool
, the developer can read the corresponding chapter.
-
int
shall be omitted from every type exceptsigned int
andunsigned int
.signed
shall also be omitted as its implied. With all that in mind, the spellings that shall be used are:bool
char
-
char8_t
(if the standard you are using supports it, though not really useful for satellite embedded programming, unless you need UTF8 codepoints, for C23 and later, import the<uchar.h>
header) -
char16_t
(not really useful for satellite embedded programming, unless you need UTF16 codepoints, for C11 and later, import the<uchar.h>
header) -
char32_t
(not really useful for satellite embedded programming, unless you want UTF32 codepoints, for C11 and later, import the<uchar.h>
header) -
wchar_t
(not really useful for satellite embedded programming, unless you want wide characters and strings, for C, import the<wchar.h>
header) signed char
unsigned char
short
unsigned short
int
unsigned int
long
unsigned long
-
long long
(C99/C++11 and later) -
unsigned long long
(C99/C++11 and later)
-
Type
char
shall not used as type to perform arithmetic with. Whetherchar
is signed or not, is not guaranteed by the standard, so writing portable code with it is not easy. Typechar
shall be used only for characters and strings in the execution encoding of the platform (ASCII is not guaranteed). Also char will promote toint
when used in arithmetic expressions. For the same reasonswchar_t
shall not be used in arithmetic.
Right
char aChar = 'A';
char jChar = 'j';
char const* aString = "SpaceDot is Recruiting";
Wrong
char foo = 5;
for (; foo < 200; foo++) { // Undefined behaviour if char is signed and CHAR_BIT = 8, due to signed overflow
...
}
-
Type
wchar_t
shall never be used. Its definition made it really difficult to handle in portable code for non trivial operations. -
In case UTF8, UTF16 or UTF32 text handling is required, the developer shall consult a senior OBSW member.
-
Standard integer type suffixes shall always be used. Said suffixes shall be written in their capital form to help with readability.
Right
int x = 5;
unsigned int y = 43U;
long z = 33L;
unsigned long p = 74UL;
long long q = 89LL;
unsigned long long r = 0xFFFULL;
Wrong
int x = 5;
unsigned int y = 43;
long z = 33;
unsigned long p = 74;
long long q = 89;
unsigned long long r = 93U;
- The developer may use whichever number base they seem fit for numeric literals. The following should be considered:
- If the number is written in a manual/datasheet/standard, use it as it is written in there
- Which number base is easier/more convenient to write that literal with (You wouldn't write a 64bit literal in binary)?
- Which is the idiomatic way of writing numbers in that context?
- Binary literals cannot be used in C prior to C23.
Example
unsigned char smallFlag = 0b10111101; // Binary (C++ only prior to C23)
unsigned long long wideFlag = 0xfff0000ff0000fffULL; // Hexadecimal
unsigned int readByOwner = 0400U; // Octal
int numberOfStudents = 50; // Decimal
- For hexadecimal literals, their capital forms shall be used.
Right
unsigned int k = 0xFFFU;
Wrong
unsigned int k = 0xfffU;
- When working with C++ (C++11 and above), the developer may use literal seperators to help with readability. Prior to C23, those seperators cannot be used, so headers that will be used in the context of both languages shall not use them.
Example
unsigned short flag = 0b1111'0101'1100'0000;
- Standard integer types should not be used in context where the values may exceed the minimum limits of those types, as defined by the standard, even if those types are able to hold a wider range (for example a 32bit
int
). If that's not possible, preprocessor conditionals shall be used to assert that the code is sound. Said limits are the following for C11 (Note: Later language standards (both C and C++) have enforced two's complement arithmetic, which means that some of these have changed):
Type | Minimum value | Maximum value |
---|---|---|
signed char | -127 | 127 |
unsigned char | 0 | 255 |
short | -32767 | 32767 |
unsigned short | 0 | 65535 |
int | -32767 | 32767 |
unsigned int | 0 | 65535 |
long | -2147483647 | 2147483647 |
unsigned long | 0 | 4294967295 |
long long | -9223372036854775807 | 9223372036854775807 |
unsigned long long | 0 | 18446744073709551615 |
-
Signed integer overflow is undefined behaviour and therefore shall be avoided. Unsigned integer overflow is well defined and performs a rollover (modulo arithmetic). Even though unsigned integers will just perform a rollover from
max{unsigned type}
to0
or from0
tomax{unsigned type}
, many times this behaviour is not desirable and for those times, use of signed arithmetic is prefered. Same applies for undeflow. -
Checks for overflow shall always be written before the actual expression that can cause it. In case the check can be done in compile time, it shall.
Wrong
// Assuming x, y > 0
int x = something();
int y = somethingElse();
assert(x > 0);
assert(y > 0);
int c = x + y;
// The compiler assumes that no undefined behaviour is invoked and will optimise away whatever the overflow check may be.
CHECK_FOR_OVERFLOW(c); // really common to just be if c < x for positive numbers, though that's not right.
Right
// Assuming x, y > 0
int x = something();
int y = somethingElse();
int c;
assert(x > 0);
assert(y > 0);
// Overflow check
if ((INT_MAX - x) < y) {// x + y > INT_MAX => INT_MAX - x < y
c = handleOverflow(x, y);
} else {
// No overflow here
c = x + y;
}
-
Divide by zero is undefined behaviour and therefore shall be avoided. Like with integer overflow, the checks for division by zero shall be performed before the actual division or modulo operation.
-
Use of subtraction with unsigned integers should be avoided, especially when dealing with indexes and sizes. In case it can't be avoided, the developer should perform an overflow check.
-
The developer shall be careful when working with small integers (
bool
,char
,signed char
,unsigned char
,short
,unsigned short
), as they all promote toint
when used in expressions. This can be the root of many bugs, especially ones related to overflow. The most dangerous of all types isunsigned short
, since even on platforms where its size is 16 bits andint
is also 16 bits, it will promote to int, handicapping its own range and invoking undefined behaviour due to signed integer overflow.
Examples
// C++ version
#include <type_traits>
unsigned short i = 234;
static_assert(not std::is_same_v<unsigned short, decltype(+i)>, "Unsigned short doesn't promote");
static_assert(std::is_same_v<decltype(+i), int>, "Unsigned short doesn't promote to int");
// C version
#include <stdbool.h>
unsigned short i = 234;
_Static_assert(_Generic(+i, unsigned short: false, default: true), "Unsigned short doesn't promote");
_Static_assert(_Generic(+i, int: true, default: false), "Unsigned short doesn't promote to int");
-
Signed and unsigned arithmetic shall not be mixed and signed numbers shall never be compared to unsigned ones. Both languages use rules that may surprise the programmer, leading to subtle at best bugs.
-
Bitwise arithmetic shall always be performed on unsigned integer types. It's easier, cleaner and less likely to invoke undefined behaviour. The developer should prefer using wide unsigned integer types to small ones, due to integer promotion.
-
Standard integer types should not be the first choice most of the times. Due to the portability of the language, these types do not have fixed sizes, making it really difficult to assess things like widths and ranges in a portable way. Instead the developer should prefer using specified width integer types.
-
Specified widths types should be preferred. They can be found in the header
<cstdio>
in C++ (C++11 and later) and<stdint.h>
in C (C99 and later). That header provides typedefs to either standard integer or extended integer types to make programming easier. Even though exact-width types (intN_t
anduintN_t
) are optional, in the platforms the team uses, they are defined and therefore shall be used. In cases where exact-width is not needed, the developer may use theint_leastN_t
anduint_leastN_t
types (they are always defined). -
Literals for these types should use
INTN_C(c)
andUINTN_C(c)
macros, at least for C code. In C++, regularT{expr}
can be used, and should be preferred. Function like casts (T(expr)
) shall not be used, to avoid narrowing. -
As with standard integer types, unsinged types shall be used when performing bitwise arithmetic.
-
The developer shall be careful when working with small integers. They may be defined using small standard integers, making them susceptible to integer promotion. To check this, the developer shall use compile time logic. The small types's maximum value shall always be used as a flag when performing complementary operations. For right shifts, a good solution is to cast the number to a wider unsigned integer that will not be promoted to
int
.
Check whether an integer is promoted
// C++ version
#include <cstdint>
#include <type_traits>
std::uint16_t value = 43;
static_assert(std::is_same_v<std::uint16_t, decltype(+value)>, "uint16_t is not promoted to int");
// C version
#include <stdbool.h>
#include <stdint.h>
uint16_t value = 43;
_Static_assert(_Generic(+value, uint16_t: false, default: true), "uint16_t is not promoted to int");
Complementary operations
#include <limits> // C++ only
#include <cstdint> // C++ only, <stdint.h> in C
std::uint16_t value = 0xff00;
static_assert(std::is_same_v<std::uint16_t, decltype(+value)>, "uint16_t is not promoted to int");
std::uint16_t comp = (~value) & UINT16_MAX; // or (~value) & std::numeric_limits<std::uint16_t>::max();
-
Shifts lead to undefined behaviour when the number is shifted more than its width. That means that if the second operand is negative or greater or equal to the width of the type, the code invokes undefined behaviour. For this, the developer can use the
std::numeric_limits<T>::digits
constant to check whether the second operand is less than the width. For C code, the expression sizeof(T) * CHAR_BIT can be used to find the width of the type. -
Right shifts should not be used in signed integer arithmetic. They are unpredictable.
-
Left shifts cause overflow. Therefore, flags should be used before shifting.
Right
#include <cstdint>
auto bigNum = std::uint32_t{ 0xFFFF'EFEF };
auto shiftedBigNum = (bigNum & std::uint32_t{ 0x00FF'FFFF }) << std::uint32_t{ 8 };
Wrong
auto bigNum = std::uint32_t{ 0xFFFF'EFEF };
auto shiftedBigNum = bigNum << 8;
Really useful article: What Every Computer Scientist Should Know About Floating-Point Arithmetic
- Floating point types shall be used when real numbers are needed.
- Floating point arithmetic shall not be used when not needed. If integers can be used, they shall be used.
- IEEE754 compliance shall not be assumed for standard floating point types. It's not guaranteed by the standard that
float
is IEEE754 single precision nor thatdouble
is IEEE754 double precision, though it's really common for them to be. In fact, it's not even guaranteed that they use 2 as their arithmetic base. In any case, the developer shall not assume that, and in case it's required, due to an algorithm being numerically stable on those formats, or relying on the error handling of those same formats, it shall be checked first. There are different ways to check this.
C++ way (recommended if available)
#include <limits>
static_assert(std::numeric_limits<double>::is_iec559, "Type `double` is not IEC 60559 compliant");
C23 way
// Full Annex F compliance
#ifndef __STDC_IEC_60559_BFP__
# error "Implementation not compliant to IEC 60559"
#endif
// Lite version of that (does not guarantee Annex F compliance)
#include <float.h>
#if !DBL_IS_IEC_60559 // FLT_IS_IEC_60559 and LDBL_IS_IEC_60559 are the other macros for this
# error "Double is not compliant to IEC 60559"
#endif
C11 way
// Full Annex F compliance
#ifndef __STDC_IEC_559__
# error "Implementation not compliant to IEC 60559"
#endif
// DIY version of FLT_IS_IEC_60559, not perfectly guaranteed conformance to IEC 60559
#include <float.h>
#if !(FLT_MANT_DIG == 24 && FLT_MAX_EXP == 128)
# error "Float is not compliant to IEC 60559"
#error
#if !(DBL_MANT_DIG == 53 && DBL_MAX_EXP == 1024)
# error "Double is not compliant to IEC 60559"
#endif
- Floating point literal suffixes shall always be used in both C and C++. They shall be used in capital form to help with readability.
Right
float t = 1.25F;
double u = 3.98;
long double b = 32.54L;
float convertCelsiusToFahreneit(float temperature) {
celsius = (5.0F / 9.0F) * (fahr - 32.0F);
}
Wrong
float t = 1.25;
double u = 3.98;
long double b = 32.54;
float convertCelsiusToFahreneit(float temperature) {
// Will lead to floating-point promotion
celsius = (5.0 / 9.0) * (fahr - 32.0);
}
-
Type
long double
shall not be used. Even thoughfloat
anddouble
use the usual formats in many platforms, forlong double
this is not the case. This makes things complicated when trying to assess precision and errors. -
Floating point exceptions shall not be ignored. The state of the floating point environment is variable and things like invalid operation can change. It should always be checked for spurious exceptions (due to division by zero, or overflow) and cleaned if needed. The exceptions that can be raised are the following:
-
FE_INVALID
: Raised from expressions like 1.25+NaN -
FE_DIVBYZER
: Raised from epressions like 1.25/0.0 -
FE_OVERFLOW
: Raised from expressions like DBL_MAX+DBL_MAX/2.0 -
FE_UNDERFLOW
: Raised from expressions like DBL_MIN-REALLY_SMALL_NUMBER -
FE_INEXACT
: Can be raised from anything really
-
-
Floating point rounding mode shall not be changed in runtime. It makes it really difficult to assess the actual results of the algorithms, making the system not that determenistic. If the developer wishes to use different rounding mode, they shall take measures to make sure it is done at compile time. For that the developer shall consult a senior OBSW member. Note that both WG14 (C) and WG21 (C++) are also in the process of deprecating manipulation of the floating point rounding mode during runtime.
-
Check for error handling in the math library. The C standard library uses the macro
math_errhandling
to define how mathematical functions inform the user that an error has occured. The macro can be used as such:
#include <cmath>
#include <cerrno>
float x = std::log(0.0F); // Erroneous input
#if math_errhandling & MATH_ERRNO // errno
checkErrno(errno);
#endif
#if math_errhandling & MATH_ERREXCEPT
checkFPException();
// Just clear all the exceptions
std::feclearexcept(FE_ALL_EXCEPT);
#endif
-
Signaling NaNs shall never be used.
-
Careful prototyping shall always be performed before an implementation running on an embedded system. First, numerical stability and correctness shall be guaranteed and then one can run it on an embedded system. Prototyping may use algebraic languages at its first stage, to ensure mathematical correctness and then it should be ported to floating point.
-
NaNs can contaminate the whole program, so checks shall be placed everywhere it's possible to create one (most notably when calling a function from the math library).
-
For C++ only, qualified names shall be used, to ensure correct dispatch of the overloaded functions (which may be macros that use
_Generic
in C). -
Errors propagate through inexact operations, so the developer shall seek ways to perform more exact operations.
-
If possible, the team should employ a member for numerical analysis (probably a mathematician), since floating point algorithm design is a really tedious process and can go wrong in many ways.
-
The program shall not perform floating point promotions, when controllable by the programmer.
-
In case the floating point environment and/or
errno
becomes a performance burden, compilation flags can be used to turn them off, if they are offered by the vendor. -
Use of
double
should not be frequent if the compiler vendor in use doesn't provide a hard-abi for it or the chip doesn't offer an FPU capable of some form of double precision (signed or unsigned). In that case, soft-float is used which can slow down the program significantly. -
Members with little or no experience shall not be assigned tasks requiring non-trivial use of floating point math.
-
Many more things here.
Future missions can take advantage of the floating point formats offered in the latest language standards (C23 and C++23 by the time of writing this), if IEC60559 is required, or exact casts from integer types are required
-
Code that runs on the satellite shall not make use of dynamic memory allocation. This ensures that the system does not perform non-determenistic operations (like
malloc
). Also that means that code shall not usealloca
and VLAs, both of which are not standard constructs in C++ and can cause a lot of problems (for them, compiler flags shall be deployed if possible to enforce their non-use). -
Non-determenistic operations shall not be used.
-
Non-local jumps shall not be used. That means that the
setjmp
/longjmp
pair cannot be used. -
Consequence of the code not being able to use dynamic memory allocation, non-local jumps and due to performace constraints, C++ exceptions shall not be used. In fact, compiler flags to turn off this feature shall be used if possible (
-fno-exceptions
). -
Due to binary size constraints, RTTI operations shall not be used. This means that no virtual/abstract classes and dynamic dispatch can be used, at least not that easily. Should the team require methodical dynamic dispatch, it will have to be implemented from scratch, as is done in the LLVM project. In fact, compiler flags to turn off this feature shall be used if possible (
-fno-rtti
). -
Inability to use dynamic memory allocation and C++ exceptions make use of the STL and other parts of the C++ standard library, impossible. Only the freestanding parts of the standard library are guaranteed to work in this context. For anything else, the programmer should ask a senior OBSW member. To compensate for that, OBSW code may use ETL to get containers.
-
Recursion shall be avoided. The reason for this, is that for arguments requiring a deep recursion tree, a stack overflow can occur, making the program useless. Note that for some architectures that may be used, recursion may not be supported.
-
#pragma once
shall be used instead of header inclusion guards.
Right
ConvolutionalEncoder.hpp
#pragma once
Wrong
#ifndef CONVOLUTIONAL_ENCODER_H
#define CONVOLUTIONAL_ENCODER_H
...
#endif // CONVOLUTIONAL_ENCODER_H
-
inline
functions shall be preferred to macros.
Right
inline square(int x) {
return x * x;
}
Wrong
#define SQUARE(x) ((x) * (x))
-
constexpr
shall be preferred to macros, unless preprocessor conditional code is required to be performed.
Right
constexpr unsigned int NumberOfStudents = 30U;
Wrong
#define NUMBER_OF_STUDENTS 30U
- Preprocessor macros shall be namespaced to avoid namespace collisions.
Right
// OQPSKMod.hpp
#define OQPSK_INPUT_LENGTH 40
#define OQPSK_SAMPLES_PER_SYMBOL 4
Wrong
// OQPSKMod.hpp
#define INPUT_LENGTH 40
#define SAMPLES_PER_SYMBOL 4
-
The directive form
#pragma
shall be used instead of the operator form_Pragma
. -
Use of
#pragma
s shall always be documented due to their implementation defined nature. One exception to this rule, are the#pragma STDC
macros, which are defined in the C standard.
-
Structs in C++ should be used for more trivial operations, just like they would be used in C.
-
Classes shall follow a particular declaration order. Public members should be written first, since that's the thing that the consumer of API cares about, protected members after that and at last, private members should be written. Same goes for non-trivial structs and unions
Right
class Foo { // Could be struct Foo, or union Foo
public:
...
protected:
...
private:
...
};
Wrong
class Foo {
private:
...
protected:
...
public:
...
};
- Uniform initialisation shall always be used for classes and structs (and sometimes unions).
Code
class FancyPoint {
public:
FancyPoint() = default;
~FancyPoint() { }
float getX() const {
return x;
}
float getY() const {
return y;
}
void setX(float nx) {
x = nx;
}
void setY(float ny) {
y = ny;
}
FancyPoint(float px, float py)
: x(px)
, y(py) { }
FancyPoint(const FancyPoint& other)
: x(other.getX())
, y(other.getY()) { }
FancyPoint(FancyPoint&& other) noexcept
: x(other.getX())
, y(other.getY()) { }
FancyPoint& operator=(const FancyPoint& other) {
if (this == &other) {
return *this;
}
this->x = other.getX();
this->y = other.getY();
return *this;
}
FancyPoint& operator=(FancyPoint&& other) noexcept {
if (this == &other) {
return *this;
}
this->x = other.getX();
this->y = other.getY();
other.setX(0.0F);
other.setY(0.0F);
return *this;
}
private:
float x;
float y;
};
Right
FancyPoint p { 12.0F, 43.32F };
// Use p
Wrong
FancyPoint p = FancyPoint(12.0F, 43.32F);
// Use p
-
Single argument constructors should be declared
explicit
most of the times. If implicit conversions are required, this rule can be ignored.
Right
class Height {
public:
explicit Height(double);
...
};
Wrong
class Height {
public:
Height(double);
...
};
Height heightOfJohn = 1.87; // Implicit conversion, not desirable here.
Right
class Point {
public:
Point(float px, float py)
: x(px)
, y(py) { }
private:
float x;
float y;
};
Wrong
class Point {
public:
Point(float px, float py)
: y(py)
, x(px) { }
private:
float x;
float y;
};
- In case construction of a class can fail, constructors shall be implemented as infallible operations and be declared as private and factory functions that can mark the error (either as out parameter, or sum type) shall be provided. The factory functions are static functions of public visibility with the name
create
(consequence of not being able to use C++ exceptions).
Example
template <typename T, typename E>
using Either = ???;
using Err = ???;
class PointInTheFirstQuadrant {
public:
~PointInTheFirstQuadrant() { }
static Result<PointInTheFirstQuadrant, Err> create(float px, float py) {
if ((px < 0.0F) or (py < 0.0F)) {
return Either::right(Err::NEGATIVE_VALUE);
}
return Either::left(PointInTheFirstQuadrant { px, py });
}
private:
PointInTheFirstQuadrant() = default;
PointInTheFirstQuadrant(float px, float py)
: x(px)
, y(py) { }
float x;
float y;
};
-
Initialisation shall be used in constructors, instead of assignment.
-
Rule of zero, three, or five should always be taken into account.
-
Unless they are used for building other types, unions should be avoided in C++. Instead, etl::variant shall be used, just like std::variant.
-
Virtual functions and their overrides shall never define default values on their arguments.
-
In C, unions may also be used for type punning, after checking that the two types are of same size (this is the idiomatic way of type punning in C, since C99).
Right
#include <stdint.h>
union FloatBits {
_Static_assert(sizeof(float) == sizeof(uint32_t), "Not of same size, cannot type pune");
float number;
uint32_t representation;
};
Wrong
#include <stdint.h>
union FloatBits {
float number;
uint32_t representation;
};
- In C, types that define their own namespace (structs, unions, enums) may use a typedef to get a typename (consistent strategy should be applied everywhere ).
- In C,
typedef
shall be used. In C++,using
shall be used. Also,typedef
shall be used in code/headers that will be consumed by both languages.
Right
// Document.cpp
using Real = float;
// Document.c
typedef float Real;
Wrong
// Document.cpp
typedef float Real;
#define Real float
// Document.c
#define Real float
- Output parameters shall be passed last in the function. In C++, lvalue references shall be used for them. One exception is when trying to mimic
this
in C. In that case, an output parameter may be passed as the first argument.
Example
SomeKindOfSignal triggerSomething(uint8_t period, Err& error);
- Trailing return types shall be used only in lambda expressions and in cases where the type must be deduced out of complicated expression.
Example
void doSomething();
int getPosition(Object& obj);
float getNormOfPoint(Point p);
// C++ only
#include <utility>
// Trailing return type will be used in the following
template<typename T, typename U>
[[nodiscard]] auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
#include <type_traits>
// lambda expression example
auto equals = [](auto a, auto b) -> bool {
static_assert(std::is_same_v<decltype(a), decltype(b)>, "Not of same type");
return a == b;
};
- Function pointer types shall be offered with a type alias.
Right
// Document.cpp
using TaskFunction = void (*)(void*);
// Document.c
typedef void (*TaskFunction)(void*);
... // In some of those files
TaskFunction fn = someFunction;
Wrong
void (*fn)(void*) = someFunction;
- Use of lambda expressions is preferred in cases where a function requires a callback function.
Right
tagRegisterStack(Tags::READ_ONLY, [](std::uintptr_t hnd) {
sanitiseHandle(hnd);
void* revitilisedHandle = reinterpret_cast<void*>(hnd);
return revitilisedHandle != nullptr;
});
Wrong
bool assessAvailabilityOfPointer(std::uintptr_t hnd) {
sanitiseHandle(hnd);
void* revitilisedHandle = reinterpret_cast<void*>(hnd);
return revitilisedHandle != nullptr;
}
...
tagRegisterStack(Tags::READ_ONLY, assessAvailabilityOfPointer);
-
Singleton types, like the ones used to define a peripheral to the embedded device (for example
struct XSpi
in Xilinx BSP) shall not be copied. In this case reference semantics (pointers) should be preferred. -
Use of
std::function
should be avoided due to dynamic memory allocation. -
The developer shall be careful when capturing variables by reference in a closure due to lifetime concerns and dangling pointer hazards.
-
The developer shall never take the address of a standard overload set. To pass one in a function expecting a function pointer, the developer shall enclose the call to the member of the overload set to lambda function literal (overload set member lifting).
- Functions that return pointers to an object, should use the prefix
maybe
in case they can return a null pointer.
Example
SomeOpaqueObj* calculateTheAttributesOfObjIfItExists(int x, int y, float z, long long q);
Example
void aCPPFunction();
void aCFunction(void);
-
The developer shall use the narrowest type of cast for the needs of the application.
-
Out of all named casts,
const_cast
shall not be used, unless some old C API requires it. -
Named casts are preferred to C style casts, which shall not be used.
-
Function style casts shall not be used.
-
dynamic_cast
shall never be used, due to the no-RTTI rule. -
static_cast
shall not be used in pointers, unless it is to convert from or to avoid*
.
To alias a piece of storage or an object means to access it. The strict aliasing rule defines that an object cannot be aliased by a type different than that which it was defined. This enables for certain optimisations and auto-vectorising sections of code. The developer ought to understand this rule in order to write code in C/C++.
-
The only types that can be used to alias an object that are different from the one used to define it, are
char
andunsigned char
. The developer shall preferunsigned char
between those two, due to the weird nature ofchar
. -
Even if the chip vendor provides APIs that transfer/move/send/receive/manipulate bytes through a protocol/network that make use of
uint8_t
(for example in ST ecosystem) oru8
(for example in Xilinx ecosystem, and other Linux-based embedded systems) or whatever type the vendor provides in the low level APIs, the developer shall prove at compile time that those types are capable of not violating the strict aliasing rule. Wrong
char message[] = "This is a message\n\r";
HAL_UART_Transmit(&huart3, (uint8_t*)messageUnsetting, sizeof(messageUnsetting), HAL_MAX_DELAY); // HAL API provided by ST
The two languages offer different ways of proving whether uint8_t
is capable of aliasing:
// C++ way
#include <type_traits>
#include <stdint.h> // probably already there by the vendor
static_assert(
std::disjunction_v<
std::is_same<std::uint8_t, unsigned char>,
std::is_same<std::uint8_t, char>
>,
"uint8_t is not capable of aliasing"
); // You could use `or` here, this was a type_traits demonstration
// C way
// For C23 and later this is not needed
#if !(defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 202311L))
# include <stdbool.h>
# include <assert.h>
#endif
#include <stdint.h> // probably already there by the vendor
static_assert(
_Generic( // You could wrap this in a macro
(uint8_t){ 0 },
unsigned char: true,
char: true,
default: false
),
"uint8_t is not capable of aliasing"
);
-
When performing type punning, the developer shall always statically assert that the sizes of the two objects is the same.
-
For code that is either written in headers that will be consumed by both languages or has to compile by both a C and a C++ compiler, or is written in C89/C++98/C++03,
memcpy
is the only accepted method of type punning. -
If C++20 is available, the developer shall use the function
std::bit_cast
from the header<bit>
(available in the C++20 freestanding library) for type punning. Said function works inconstexpr
context and also checks whether the transform is possible or not (size and layout checking). -
If ETL is available, the function
etl::bit_cast
from the header<etl/bit.h>
shall be used for type punning. Note that the underlying implementation of it doesn't allow for use inconstexpr
context (at least not before C++26). In case there is a need for type punning in constexpr context, consult a senior OBSW member. -
If C99 (or later) is available, unions shall be used for type punning. Size checks shall be performed for the definition of the union (in more detail, a static_assert can be inserted in the union that asserts that the sizes of the two types are the same, since for different variant sizes, a read in a variant that was not used for write and has a bigger size than the one the write was performed on, the behaviour is unspecified).
- Template keyword and its parameters shall be on a seperate line.
Right
template<typename T, std::size_t N>
T addElementsOfAnArray(T (&arr)[N]);
Wrong
template<typename T, std::size_t N> T addElementsOfAnArray(T (&arr)[N]);
- For templates that aim to be type generic functions/classes, the keyword
typename
shall be used instead ofclass
.
Right
template<typename T>
T add(T a, T b);
Wrong
template<class T>
T add(T a, T b);
-
Templates shall not be exported or split in any way. They should alway be implemented in a header, if they are parts of a public API.
-
No space shall be inserted between the keyword
template
and the opening angle bracket.
Right
template<typename T>
T add(T a, T b);
Wrong
template <typename T>
T add(T a, T b);
- Use of templates should be moderate, since they can be the cause of binary size explosion, as well as slow compile times. Note that this rule does not prohibit their use as many such guides tend to do, though it's expected that the developer knows how to work with templates. For this, members with little or no experience shall not be assigned to work with templated code.
- C++17 declarations inside control statements should be used if they make code quality better.
Right
if (auto q = someLine.getPoint(); q.isInTheFirstQuadrant()) {
... // Use q
}
Wrong
{
auto q = someLine.getPoint();
if (q.isInTheFirstQuadrant()) {
... // Use q
}
}
- Whenever possible, a
range-for
loop shall be used.
Right
#include <etl/array.h>
#include <iostream>
etl::array<int, 5> arr { 3, 2, 3, 6, 7 };
for (auto elem : arr) {
std::cout << elem << " ";
}
std::cout << std::endl;
Wrong
#include <etl/array.h>
#include <iostream>
etl::array<int, 5> arr { 3, 2, 3, 6, 7 };
for (std::size_t i = 0; i < arr.size(); i++) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
-
do while
loops should be avoided, unless it's the idiomatic way to write some piece of code.
- West cv qualifiers shall be used.
Right
const int x = 5;
const volatile long = 8L;
Wrong
int const x = 5;
long const volatile = 8L;
-
Variables and formal parameters that might be unused should be marked with
[[maybe_unused]]
. -
Use of
[[nodiscard]]
is recommended to enforce the API use to treat with errors and not leak resources (for example when returning handles). Types that will be used only for return values, should be marked as[[nodiscard]]
. -
The attribute
[[fallthrough]]
shall always be used when that behaviour is the intended one in aswitch
statement. -
When using non-standard attributes, standard attribute syntax shall be used. Consult your vendor's compiler manual to find out in which namespace they reside.
Right
struct AStruct {
char c;
int i;
};
struct [[gnu::packed]] MyPackedStruct {
char c;
int i;
AStruct s;
};
Wrong
struct AStruct {
char c;
int i;
};
// Non standard syntax can be anything:
// __attribute__((foo)) for gnu style attributes
// __declspec(foo) for microsoft style attributes (though for packing one has to use a #pragma)
// __foo for IAR attributes
// Check your vendor's manual
struct __attribute__((packed)) MyPackedStruct {
char c;
int i;
AStruct s;
};
- In case a packed struct/union is required, the developer shall always statically assert that the size and/or the alignment of the type is the intended one.
-
errno
shall not be used for error handling in user defined functions. Though it should be checked in case a function can change its value. -
setjmp
/longjmp
shall never be used, since they perform non-local jumps and they do not respect C++ destructors completely. -
C++ exceptions shall not be used, since they are not determenistic, they dynamically allocate memory and they perform non-local jumps.
-
errno
-like structures or values shall not be used, since they are external state (many times even global, if the user or the platform doesn't know how to handlethread_local
qualified variables), which makes functions that use it non-pure, hindering optimisations. -
Use of sum types is recommended for error handling. Said sum types can be
etl::optional
andetl::expected
. The developer should choose the simplest of the types that can express the error condition, which means that when the error path doesn't report anything useful, `etl::optional should be used. -
For trivial cases, a
bool
failure indicator as a return value, or an error code can be used for error handling. This case should be used when the return channel won't return anything.
- Precondition, postcondition and invariant violations ought to be promoted to recoverable errors, meaning they should be treated just like every other error condition.
This section unfortunately can't be finalised yet, due to a bug in the ETL library and the deprecation of etl::result
, though the plan is to upgrade to a newer ETL version to use etl::expected
- Use of
etl::span
shall be preferred as a function parameter instead of references toetl::array
oretl::vector
(or any other contiguous data container).
Right
void printNumberArray(etl::span<const int, 5> numbers);
void incrementAllNumbers(etl::span<int> numbers);
int findLargestNumber(etl::span<const int> numbers);
Wrong
void printNumberArray(const etl::array<int, 5>& numbers);
void incrementAllNumbers(const etl::vector<int, 30>& numbers);
int findLargestNumber(const int* numbers, std::size_t size);
-
Low level operations (ex.
memcpy
) should be avoided on high level structures, like vectors, maps, trees, tries, etc. -
Views (span, string_view, etc.) should not be used as return types to avoid dangling reference/pointer issues.
By conan, the document, by the time of writing, refers to conan 2.0 which had breaking changes in both python and text interfaces
-
Third party packages shall be consumed via the Conan package manager.
-
Individual software modules shall be packaged using conan.
By the time of writing, a migration to Conan is in progress