Embedded C++ Guide#

Pigweed AI summary: This page provides recommendations for using C++ in embedded software, which can serve as a resource for efficiently using C++ in external projects. The recommendations cover topics such as constexpr functions and variables, function templates, virtual functions, compiler warnings, dealing with unused variables and functions, and dealing with nodiscard return values. The recommendations are subject to change as the C++ standard and compilers evolve, and feedback is welcome to improve the guide or correct any inaccuracies.

This page contains recommendations for using C++ for embedded software. For Pigweed code, these should be considered as requirements. For external projects, these recommendations can serve as a resource for efficiently using C++ in embedded projects.

These recommendations are subject to change as the C++ standard and compilers evolve, and as the authors continue to gain more knowledge and experience in this area. If you disagree with recommendations, please discuss them with the Pigweed team, as we’re always looking to improve the guide or correct any inaccuracies.

Constexpr functions#

Pigweed AI summary: Constexpr functions are functions that can be called from a constant expression, but labeling a function as constexpr does not guarantee that it will be executed at compile time. These functions are implicitly inline and suitable to be defined in header files. However, marking non-trivial functions as constexpr could increase code size. Simple constructors should be marked constexpr whenever possible, but important initialization should not be avoided just to make the class constexpr-constructible unless it needs to be used in a constant expression.

Constexpr functions are functions that may be called from a constant expression, such as a template parameter, constexpr variable initialization, or static_assert statement. Labeling a function constexpr does not guarantee that it is executed at compile time; if called from regular code, it will be compiled as a regular function and executed at run time.

Constexpr functions are implicitly inline, which means they are suitable to be defined in header files. Like any function in a header, the compiler is more likely to inline it than other functions. Marking non-trivial functions as constexpr could increase code size, so check the compilation results if this is a concern.

Simple constructors should be marked constexpr whenever possible. GCC produces smaller code in some situations when the constexpr specifier is present. Do not avoid important initialization in order to make the class constexpr-constructible unless it actually needs to be used in a constant expression.

Constexpr variables#

Pigweed AI summary: This section discusses the use of constexpr variables in C++. Constexpr variables are marked with the "constexpr" keyword and can be used in constant expressions, such as non-type template arguments and static_assert statements. They can be initialized at compile time with values calculated by constexpr functions. Constexpr variables are implicitly const, so there is no need to include the const qualifier when declaring them. However, unlike constexpr functions, constexpr variables are not implicitly inline and must be declared with the inline specifier in headers. The

Constants should be marked constexpr whenever possible. Constexpr variables can be used in any constant expression, such as a non-type template argument, static_assert statement, or another constexpr variable initialization. Constexpr variables can be initialized at compile time with values calculated by constexpr functions.

constexpr implies const for variables, so there is no need to include the const qualifier when declaring a constexpr variable.

Unlike constexpr functions, constexpr variables are not implicitly inline. Constexpr variables in headers must be declared with the inline specifier.

namespace pw {

inline constexpr const char* kStringConstant = "O_o";

inline constexpr float kFloatConstant1 = CalculateFloatConstant(1);
inline constexpr float kFloatConstant2 = CalculateFloatConstant(2);

}  // namespace pw

Function templates#

Pigweed AI summary: Function templates allow for writing code that can work with different types of values. The example given is a function called Clamp, which can clamp a value within a minimum and maximum. The function can work with any type of value, including custom types that support the < and > operators. The compiler generates a separate version of the function for each set of types it is instantiated with, which can increase code size significantly. It is important to be careful when instantiating non-trivial template functions with multiple types.

Function templates facilitate writing code that works with different types. For example, the following clamps a value within a minimum and maximum:

template <typename T>
T Clamp(T min, T max, T value) {
  if (value < min) {
    return min;
  }
  if (value > max) {
    return max;
  }
  return value;
}

The above code works seamlessly with values of any type – float, int, or even a custom type that supports the < and > operators.

The compiler implements templates by generating a separate version of the function for each set of types it is instantiated with. This can increase code size significantly.

Tip

Be careful when instantiating non-trivial template functions with multiple types.

Virtual functions#

Pigweed AI summary: Virtual functions provide runtime polymorphism, but should only be used when necessary. They require a virtual table, which increases RAM usage and can inhibit compiler optimizations. When runtime polymorphism is needed, virtual functions should be considered over alternatives like a struct of function pointers, which sacrifice flexibility and ease of use without offering performance advantages. The tip is to only use virtual functions when necessary.

Virtual functions provide for runtime polymorphism. Unless runtime polymorphism is required, virtual functions should be avoided. Virtual functions require a virtual table, which increases RAM usage and requires extra instructions at each call site. Virtual functions can also inhibit compiler optimizations, since the compiler may not be able to tell which functions will actually be invoked. This can prevent linker garbage collection, resulting in unused functions being linked into a binary.

When runtime polymorphism is required, virtual functions should be considered. C alternatives, such as a struct of function pointers, could be used instead, but these approaches may offer no performance advantage while sacrificing flexibility and ease of use.

Tip

Only use virtual functions when runtime polymorphism is needed.

Compiler warnings#

Pigweed AI summary: This article discusses the importance of compiler warnings in identifying and fixing bugs in embedded systems. Pigweed compiles with a strict set of warnings, including -Wall, -Wextra, -Wimplicit-fallthrough, and -Wundef. The article also provides tips for addressing warnings about unused variables or functions, such as deleting them or annotating them with [[maybe_unused]]. Finally, the article explains how to deal with nodiscard return values, such as using .IgnoreError() for pw::Status

Bugs in embedded systems can be difficult to track down. Compiler warnings are one tool to help identify and fix bugs early in development.

Pigweed compiles with a strict set of warnings. The warnings include the following:

  • -Wall and -Wextra – Standard sets of compilation warnings, which are recommended for all projects.

  • -Wimplicit-fallthrough – Requires explicit [[fallthrough]] annotations for fallthrough between switch cases. Prevents unintentional fallthroughs if a break or return is forgotten.

  • -Wundef – Requires macros to be defined before using them. This disables the standard, problematic behavior that replaces undefined (or misspelled) macros with 0.

Unused variable and function warnings#

Pigweed AI summary: This section discusses how to address warnings about unused variables or functions in code. The best way to address these warnings is to remove the unused items, but in some cases, they cannot be removed, so the warning must be silenced. The section provides several ways to silence these warnings, including deleting unused entities, avoiding giving them a name, annotating them with [[maybe_unused]], and casting them to void. In C, silencing warnings on unused functions may require compiler-specific attributes.

The -Wall and -Wextra flags enable warnings about unused variables or functions. Usually, the best way to address these warnings is to remove the unused items. In some circumstances, these cannot be removed, so the warning must be silenced. This is done in one of the following ways:

  1. When possible, delete unused variables, functions, or class definitions.

  2. If an unused entity must remain in the code, avoid giving it a name. A common situation that triggers unused parameter warnings is implementing a virtual function or callback. In C++, function parameters may be unnamed. If desired, the variable name can remain in the code as a comment.

    class BaseCalculator {
     public:
      virtual int DoMath(int number_1, int number_2, int number_3) = 0;
    };
    
    class Calculator : public BaseCalculator {
      int DoMath(int number_1, int /* number_2 */, int) override {
        return number_1 * 100;
      }
    };
    
  3. In C++, annotate unused entities with [[maybe_unused]] to silence warnings.

    // This variable is unused in certain circumstances.
    [[maybe_unused]] int expected_size = size * 4;
    #if OPTION_1
    DoThing1(expected_size);
    #elif OPTION_2
    DoThing2(expected_size);
    #endif
    
  4. As a final option, cast unused variables to void to silence these warnings. Use static_cast<void>(unused_var) in C++ or (void)unused_var in C.

    In C, silencing warnings on unused functions may require compiler-specific attributes (__attribute__((unused))). Avoid this by removing the functions or compiling with C++ and using [[maybe_unused]].

Dealing with nodiscard return values#

Pigweed AI summary: This section discusses how to handle situations where a "nodiscard" return value from a function call needs to be discarded. For "pw::Status" value, ".IgnoreError()" can be used, while for other instances, "std::ignore" can be used. The code example shows the use of these methods.

There are rare circumstances where a nodiscard return value from a function call needs to be discarded. For pw::Status value .IgnoreError() can be appended to the the function call. For other instances, std::ignore can be used.

// <tuple> defines std::ignore.
#include <tuple>

DoThingWithStatus().IgnoreError();
std::ignore = DoThingWithReturnValue();