Compile-Time Polymorphism

By | 2014-11-12

Let’s say we have a peripheral, like an accelerometer, connected via an SPI bus. Then let’s say we have to different embedded projects that make use of this same chip, implemented on two different microcontrollers. Wouldn’t it be nice to be able to write the accelerometer communication code once, keep a low runtime overhead that we are always seeking in embedded projects, and yet defer the low-level SPI bus writes to the appropriate code for each microcontroller?

If we had infinite resources we might do this:

class TAccelerometer {
  public:
    Accelerometer() {}
    virtual Accelerometer() {}

    bool setup();
    void shutdown();
    uint8_t read();

  protected:
    virtual uint8_t readWriteSPI(uint8_t out) = 0;
    virtual void select() = 0;
    virtual void deselect() = 0;
};

// in project #1
class TAccelerometerOnCPU1 : public Accelerometer {
  protected:
    uint8_t readWriteSPI(uint8_t out) override;
    void select() override;
    void deselect() override;
};

// in project #2
class TAccelerometerOnCPU2 : public Accelerometer {
  protected:
    uint8_t readWriteSPI(uint8_t out) override;
    void select() override;
    void deselect() override;
};

We put all the high-level accelerometer commands in the public member functions and override the virtuals to suit the hardware for CPU1 and CPU2. Great. Except on an embedded system we’ve paid for a whole set of virtual calls; and possibly (depending on how clever the compiler is) the code for a class we’re not using.

As an alternative, we can do a kind of compile-time equivalent; we do this with a dependency injection done via a template.

template <typename TSPI>
class TAccelerometer {
  public:
    bool setup();
    void shutdown();
    uint8_t read();

  protected:
    uint8_t readWriteSPI(uint8_t out) { return TSPI::readWriteSPI(out); }
    void select() { return TSPI::select(); }
    void deselect() { return TSPI::deselect(); }
};

// in project #1
class TSPIForCPU1 {
  public:
    static uint8_t readWriteSPI(uint8 out);
    static uint8_t select();
    static uint8_t deselect();
};

// in project #2
// ... same again ...

Now we have no run time overhead; the compiler has enough information to inline and directly call all the hardware-specific functions with no runtime overhead. We can make this more pleasant by using our old friend the curiously recurring template pattern.

template <typename TSPI>
class TAccelerometer {
  private:
    TAccelerometer();

  public:
    static bool setup();
    static void shutdown();
    static uint8_t read();

  protected:
    static uint8_t readWriteSPI(uint8_t out) { return TSPI::readWriteSPI(out); }
    static void select() { return TSPI::select(); }
    static void deselect() { return TSPI::deselect(); }
};

// in project #1
class TSPIForCPU1 : public TAccelerometer<TSPIForCPU1> {
  public:
    static uint8_t readWriteSPI(uint8 out);
    static uint8_t select();
    static uint8_t deselect();
};

In some ways this second version is more representative of the truth – with all the functions static, we’re saying that there is no such thing as a runtime instance of this class. That’s a sensible way of looking at it, as it’s not like we could ever instantiate two – there is one bit of hardware, and these calls always operate on it.

There is still an annoyance though, which I’ve ignored in the above examples, is that the implementation would have to be entirely in the header for this to work. We can’t get away from the fact that both the “base” and “override” have to be available at compile time when we’re dealing with templates, but we can perhaps improve on keeping the whole implementation inline in headers.

Leave a Reply