Modern C++ in embedded development: Static Classes
There is a concept of static class in C#. It is a class that contains only static members and methods, and it can’t be instantiated. In C#, a static class is declared using the static keyword.
Static classes are used to group functions that belong to the same logical unit or software module and that may have a shared state (member variables).
Static class in C++
The concept of a static class can be implemented in C++ as a class with all static methods and members and by deleting the default constructor. Deleted constructor will prevent accidental instantiation during compile time. But why is disabling instantiation important for a static class? Well, it is about declaring a clear intention to the user of the class: “The function you are using belongs to a static class. It doesn’t have internal states specific to an instance as there are no instances, and if there is an internal state, it will affect anyone using this class.”
Why would we want to have a concept of class with all static methods and members when we can use a namespace for the same purpose? The answer is templates. We can instantiate a template class with a type (a class) but not with a namespace.
Some good candidates for static class implementation in C++ in embedded systems would be PowerManager, SystemControl, Flash, and similar classes that do not require instances. FileSystem, I2C, UART, and SPI could also be candidates, but usually, there is more than one serial interface, and sometimes there is a need for multiple file systems.
Following is an example of Stm32PowerManager static class:
struct Stm32PowerManager { Stm32PowerManager() = delete; static void switchToLowPowerMode() { printf("STM32 switching to low power mode...\n"); // Implementation for STM32-specific operations to switch to low power mode } static void switchToNormalMode() { printf("STM32 switching to normal mode...\n"); // Implementation for STM32-specific operations to switch to normal mode } static void monitorBatteryAndSleep() { printf("Monitoring battery...\n"); // Placeholder for monitoring battery and deciding to sleep // If battery level is below threshold: // switchToLowPowerMode(); } };
Stm32PowerManager is a stateless class with all static methods, which makes it a great fit for a static class. Now, let's assume that the Stm32PowerManager will be used in the main loop that we will implement as a struct Main with one static method loop:
template<PowerManager P> struct Main { static void loop() { printf("Main loop started...\n"); // Just as an example: switch modes and monitor battery in the loop P::switchToNormalMode(); P::monitorBatteryAndSleep(); printf("Main loop ended...\n"); } };
Main is a templated struct that utilizes the PowerManager and acts as a utility that can work with any type that satisfies the PowerManager concept. It leverages the type-safety and compile-time checking provided by concepts in C++20, ensuring that a user doesn't mistakenly use the Main struct with a type that doesn't have the required power management capabilities.
The concept PowerManager is defined as follows:
template<typename T> concept PowerManager = requires { { T::switchToLowPowerMode() } -> std::same_as<void>; { T::switchToNormalMode() } -> std::same_as<void>; { T::monitorBatteryAndSleep() } -> std::same_as<void>; };
This design makes the business logic of our simple main loop platform independent. We can use it with different platform-specific PowerManager classes, making it both portable and testable off the target.
Here is an example of the usage of the Stm32PowerManager class as a template argument of the templated Main class:
int main() { Main<Stm32PowerManager>::loop(); return 0; }
Testability
Static classes are notorious for their testability due to the global states they may hide, so the rule of thumb is to avoid any states tied to a static class. Of course, that is not always possible.
If you want to make sure that the firmware enters low-power mode in specific scenarios, you can make a wrapper that meets PowerManager concept requirements as follows:
struct MockPowerManager { static Mock* mock; static void switchToLowPowerMode() { mock->switchToLowPowerMode(); } // … };
You set up a mock pointer in your tests and then check all expectations against it. It’s a bit ugly, but it's still possible to test classes that are using static classes by writing a wrapper similar to this one.
Conclusion
Static classes are a great way to group related functions under a single hat. Making them stateless is preferred but not always possible. They will enhance your firmware design, making the business logic hardware independent and testable.
- Comments
- Write a Comment Select to add a comment
Here is the Godbolt link for the full example: https://godbolt.org/z/EG6MYPETY
To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.
Please login (on the right) if you already have an account on this platform.
Otherwise, please use this form to register (free) an join one of the largest online community for Electrical/Embedded/DSP/FPGA/ML engineers: