SpaceDot Coding Guidelines - PeakSat/obc-software GitHub Wiki

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).

Notes

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.

Language standards

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 Commits

  • 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).

Code style guidelines

Indentation

  • 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;

Spacing

  • 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};

Line breaking

  • 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 for else if statements

Rigth

if (condition) {
    ...
} else {
    ...
}

if (condition) {
    ...
} else if (anotherCondition) {
    ...
} else {
    ...
}

Braces

  • 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);

Null pointers

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) {
    ...
}

Naming conventions

  • 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, use camelCase 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 plain enum 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 plain enum, 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,
};

Boolean expressions and types

  • When writing in C++ (or C23 and above), use the standard bool type and the standard true and false 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 the bool macro (which expands to _Bool, so _Bool is actually the boolean type) and true and false 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, but bool, 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 a static_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. For if statements in which the condition is something along the lines of not condition, prefer the alternative spelling of the logical NOT operator for better readability.

Prefer this

if (not someCondition()) {
    ...
}

to this

if (!someCondition()) {
    ...
}

Standard integer types

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 except signed int and unsigned 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. Whether char is signed or not, is not guaranteed by the standard, so writing portable code with it is not easy. Type char shall be used only for characters and strings in the execution encoding of the platform (ASCII is not guaranteed). Also char will promote to int when used in arithmetic expressions. For the same reasons wchar_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
    ...
}

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;

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} to 0 or from 0 to max{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 to int when used in expressions. This can be the root of many bugs, especially ones related to overflow. The most dangerous of all types is unsigned short, since even on platforms where its size is 16 bits and int 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.

Extended and 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 and uintN_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 the int_leastN_t and uint_leastN_t types (they are always defined).

  • Literals for these types should use INTN_C(c) and UINTN_C(c) macros, at least for C code. In C++, regular T{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();

More on integer arithmetic

  • 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;

Floating point arithmetic

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 that double 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 though float and double use the usual formats in many platforms, for long 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

Requirements and limitations

  • 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 use alloca 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.

Preprocessor

  • #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 #pragmas 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.

Classes, structs and unions

  • 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:
    ...
};

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

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;
};

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 ).

Type alias

  • 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

Functions

  • 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);

Type casting

  • 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 a void*.

Type punning and strict aliasing rule

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 and unsigned char. The developer shall prefer unsigned char between those two, due to the weird nature of char.

  • 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) or u8 (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 in constexpr 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 in constexpr 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).

Templates

  • 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 of class.

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.

Control statements

  • 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
    }
}

Loops

  • 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.

Qualifiers

  • West cv qualifiers shall be used.

Right

const int x = 5;
const volatile long = 8L;

Wrong

int const x = 5;
long const volatile = 8L;

Characters, strings and string literals

Attributes

  • 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 a switch 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.

Compile time execution

Error handling

  • 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 handle thread_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 and etl::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

ETL

  • Use of etl::span shall be preferred as a function parameter instead of references to etl::array or etl::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.

Documentation and comments

C++ methodologies

RAII

SFINAE

CTAD

CRTP

Logger

Package management

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

⚠️ **GitHub.com Fallback** ⚠️