Close

Avoid vtable, big RAM/flash gains, upgrade to C++23

A project log for Hardware Data Logger

Easily extendable data logging platform with STM32F103RBTx, WiFi, microSD storage, LCD with four buttons, UART, and pulse counters.

robert-gawronRobert Gawron 01/04/2026 at 19:080 Comments

I've gained 5.97% (9584 bytes) of flash and 8.29% (1696 bytes) of RAM, mainly by completely removing usage of virtual functions, but probably also because I've moved to C++23 and used its features and updated GCC to the latest stable version (because C++23 is still a bit under development on the GCC/Clang side).

In this post I will give a cool example of how I've avoided virtual functions and used (a bit pointlessly but it's in compilation time) modern C++23 features. 

The thing is that I've structured my application into three layers: BusinessLogic, Device and Driver. All HAL related code is in Driver. There I have for example SdCardDriver, UartDriver and other classes that wrap/hide low level hardware subjects.

Previously I've created for each of such class an abstraction class, so I had ISdCardDriver, IUartDriver etc with all pure virtual methods.

For the build of the firmware, the Driver's classes inherited from it (and implemented its methods using HAL's methods). For the build of PC simulation (the firmware for testing and simulation can be run on PC, no need for hardware, I've presented this in previous entries) I have a different set of Drivers that implemented the same abstraction classes (and they provide mocks too). 

Depending on whether I was building binary for ARM or binary for PC I've switched the compiler and the Driver folder that I used so the correct set of drivers would be used.

So far so good, it works and is correct, but now all those methods are virtual and it adds a bit of time of execution and binary size (under the hood compiler adds virtual table and in runtime checks what methods it should call).

I've realized that I could do better :)

Now I don't have those abstraction classes at all, I didn't need them. Build for ARM has its folder with hpp/cpp that uses HAL and build for PC has its own too. The idea of switching folder with drivers depending on build type stays the same. Common logic (like enums etc.) that are used by both AR/PC variants were extracted to common headers so that there is no duplication.

And that could be the end but C++23 has a nice feature... concepts.

An example of concept from project's code:

    template <typename T>
    concept SdCardDriverConcept =
        std::derived_from<T, DriverComponent> &&
        requires(T driver,
                 std::string_view filename,
                 FileOpenMode mode,
                 std::span<const std::uint8_t> data) {
            // File operations
            { driver.openFile(filename, mode) } noexcept -> std::same_as<SdCardStatus>;
            { driver.write(data) } noexcept -> std::same_as<SdCardStatus>;
            { driver.closeFile() } noexcept -> std::same_as<SdCardStatus>;
        };

For each driver I've added concept that checks (during compilation) if the driver implements all needed methods, for example:

   class SdCardDriver : public DriverComponent
    {
      //...
    };

    static_assert(Driver::Concepts::SdCardDriverConcept<SdCardDriver>,
                  "SdCardDriverConcept must satisfy the concept requirements");

It's a static assertion. It's not needed here, if one class doesn't implement all that is needed the build would fail anyway, anyway nice to have it.

Before all those optimization:

Memory region         Used Size  Region Size  %age Used
             RAM:        6608 B        20 KB     32.27%
           FLASH:       35916 B       128 KB     27.40%

After:

[22/22] Linking CXX executable HardwareDataLogger_STM32F103RBTx.elf
Memory region         Used Size  Region Size  %age Used
             RAM:        4912 B        20 KB     23.98%
           FLASH:       28088 B       128 KB     21.43%

Discussions