0

Overview

Long story short. I've implemented a self written framework for Stm32F7 and Stm32F4 in order to learn C++. On my first draft I heavily relied on virtual functions but in a recent task I got performance issues using that design (microsecond timing relevant task).

So I tryed to use static polymorphism but I'm not sure if that's a good solution. I'll explain my issues in the examples provided.

Overview:
I have an interface for GPIO functions. In this reduced example just write Then there are different MCU specific implementations of this interface. I just provided the F7 Example but others should be possible. And lastly I have Domain specific code. In this example some Sensor that fetches uses a GPIO internally in order to fetch someting.

Important: The Sensor should not rely on the specific hardware. It should rely on the abstraction of GPIO provided.

Solutions I've found other than virtual functions

1: CRTP

// gpio interface
namespace V1 {
template<class D>
class Gpio
{
public:
    void write(bool status) {
        static_cast<D*>(this)->impl_write(status);
    };
};
}

// f7 specific gpio
namespace V1 {
class F7 : public V1::Gpio<F7> {
public:
    void impl_write(bool status) {

    }
};
}

// sensor
namespace V1 {
template <class GPIO_T>
class Sensor {
private:
    Gpio<GPIO_T> *gpio;
public:
    Sensor(Gpio<GPIO_T> *gpio) : gpio(gpio) {}
    void read() {
        return gpio->write(true);
    }
};
}

// main
using gpio_f7 = V1::F7;

V1::Gpio<gpio_f7> gpio {};
V1::Sensor<gpio_f7> sensor {&gpio};

This code is actually working and also removing the call overhead from virtual functions in compile time. But it has quite some drawbacks.

As I rely on template parameters everything has to be header only.

I can't store the abstraction Gpio *gpio as the base (V1::Gpio) needs the deriviate as template, so it has to be Gpio<GPIO_T> *gpio. The issue with that is that this results in EVERY CLASS that uses my abstractions to be templetized (like GPIO_T above). Which for me feels like a huge deficit. But as I'm not in C++ industry maybe this is completly fine ?

2: Template functions

The other option I've found is a C-Style template functions api.

// interface
namespace V2 {
template<class T>
void write(T& gpio, bool status) {
    gpio->write(status);
}
}

// gpio
namespace V2 {
class F7 {
public:
    void write(bool status) {

    }
};
}

// sensor
namespace V2 {
class Sensor {
private:
    F7 *gpio;
public:
    Sensor(F7 *gpio) : gpio(gpio) {

    }
    void read() {
        return write(gpio, true);
    }
};
}

// main
V2::F7 gpio {};
V2::Sensor sensor {&gpio};

IMO the C-Style API is for me a big disadvantage. As this hides the functions its easier to see which functions are available on an object using an IDE. (Maybe functions are hidden behind different include files)

furthermore I cant store the Abstraction the the Sensor anymore and have to directly annotate the F7 classes. Therefore I would have to change EVERY class if I want to use a different MCU.

So I'm actually not really happy with both solutions. Atm I'm tending to rewrite my classes in CRTP-style. As at least for the most important most used classes like GPIO I would rather have the most performance I can archieve.

So my questions are:

Am I using CRTP wrong ? Is there a way to get rid of templetizing every class that uses the abstractions ? Or is it okay (idiomatic?) to templetize every class ? Are there other solutions (C++ Version doesn't matter can be C++20)?

Community
  • 1
  • 1
Nextar
  • 1,088
  • 2
  • 12
  • 26
  • 1
    *As I rely on template parameters everything has to be header only.* That should not matter a lot. Being embedded software, it won't be so huge that compile times matter a lot. If I were you, I would value the ability of the compiler to optimize the hell out of my code a lot more than being able to stash code away in non-header files. At least that's what I did with my embedded projects. – j6t Mar 29 '20 at 13:47
  • I mean if this doesn't seem inconvenient from c++ prospective I think I'm fine with using the CRTP variant. – Nextar Mar 29 '20 at 15:51

1 Answers1

0

In your first solution using CRTP, if you're already providing the V1::F7 as the template parameter, then what's the point of keeping a Gpio<GPIO_T> * in the sensor? You could have simply stored a GPIO_T m_impl and used that. As you said the problem is that takes away the need that the implementation should comply to the interface, and that for each implemenation, Sensor<IMPL_TYPE> is a different type.

So what you desire is a way of type-erasure that's faster than using virtual functions. If you already know the implementations before-hand, you can try using std::variant:

#include <variant>

// gpio interface
namespace V1 {
class Gpio
{
public:
    virtual void write(bool) = 0;
};
}

// f7 specific gpio
namespace V1 {
class F7 : public V1::Gpio {
public:
    void write(bool status) override {

    }
};
}
// f8 specific gpio
namespace V1 {
class F8 : public V1::Gpio {
public:
    void write(bool status) override {

    }
};
}


// sensor
namespace V1 {
class Sensor {
private:
    std::variant<F7,F8> gpio;
public:
    template <typename T>
    Sensor(const T& t) : gpio(t) {}
    void read() {
        std::visit([](auto&& arg) { arg.write(true); },gpio);
    }
};
}

int main()
{
    V1::F8 impl;
    V1::Sensor s(impl);
    s.read();
    return 0;
}

Though as per this instead of std::visit, you can try std::get_if since that's marked constexpr

theWiseBro
  • 1,439
  • 12
  • 11
  • I'll take a deeper look into `std::variant` and whats actually happening in `std::visit`. But I think with this solution I would still be forced to know possible implementation beforehand. – Nextar Mar 29 '20 at 15:49
  • @Nextar If you want things to be done at compile-time, the compiler at least needs to know the types. Else its runtime which is best done by virtual functions – theWiseBro Mar 29 '20 at 19:15