2

I am trying to understand the internals of https://github.com/vshymanskyy/TinyGSM/tree/master/src and am confused with how the classes are constructed.

In particular I see that in TinyGsmClientBG96.h they define a class that inherits from multiple templated parent classes.


class TinyGsmBG96 : public TinyGsmModem<TinyGsmBG96>,
                    public TinyGsmGPRS<TinyGsmBG96>,
                    public TinyGsmTCP<TinyGsmBG96, TINY_GSM_MUX_COUNT>,
                    public TinyGsmCalling<TinyGsmBG96>,
                    public TinyGsmSMS<TinyGsmBG96>,
                    public TinyGsmTime<TinyGsmBG96>,
                    public TinyGsmGPS<TinyGsmBG96>,
                    public TinyGsmBattery<TinyGsmBG96>,
                    public TinyGsmTemperature<TinyGsmBG96>

Fair enough. If I look at one of these, for example TinyGsmTemperature, I find some confusing code.

It looks like the static cast is in place so the we can call the hardware agnostic interface getTemperature() and use the implementation defined in TinyGsmBG96.

  • Why not use function overriding in this case?
  • What is the thinking behind this implementation?
  • Is this a common pattern in c++?
template <class modemType>
class TinyGsmTemperature
{
public:
  /*
   * Temperature functions
   */
  float getTemperature()
  {
    return thisModem().getTemperatureImpl();
  }

  /*
   * CRTP Helper
   */
protected:
  inline const modemType &thisModem() const
  {
    return static_cast<const modemType &>(*this);
  }
  inline modemType &thisModem()
  {
    return static_cast<modemType &>(*this);
  }

  float getTemperatureImpl() TINY_GSM_ATTR_NOT_IMPLEMENTED;
};

1 Answers1

1

Is this a common pattern in c++?

Yes, it is called CRTP - curiously recurring template pattern.

Why not use function overriding in this case?

override relies on virtual tables, causing extra runtime overhead.

What is the thinking behind this implementation?

Say, we want a class hierarchy with overridable methods. The classic OOP approach is virtual functions. However, they aren't zero-cost: when you have

void foo(Animal& pet) { pet.make_noise(); }

you don't statically know (in general) which implementation has been passed to foo() because you've erased its type from Dog (or Cat? or something else?) to Animal. So, the OOP approach uses virtual tables to find the right function at runtime.

How do we avoid this? We can instead remember the derived type statically:

template<typename Derived /* here's where we keep the type */> struct Animal {
    void make_noise() {
        // we statically know we're a Derived - no runtime dispatch!
        static_cast<Derived&>(*this).make_noise();
    }
};
struct Dog: public Animal<Dog /* here's how we "remember" the type */> {
    void make_noise() { std::cout << "Woof!"; }
};

Now, let's rewrite foo() in a zero-cost manner:

template<typename Derived> void foo(Animal<Derived>& pet) { pet.make_noise(); }

Unlike the first attempt, we haven't erased the type from ??? to Animal: we know Animal<Derived> is actually a Derived, which is a templated - therefore, fully known to the compiler - type. This turns the virtual function call into a direct one (so, even allows inlining).

passing_through
  • 1,778
  • 12
  • 24
  • 1
    more info on virtual functions [here](https://stackoverflow.com/questions/2391679/why-do-we-need-virtual-functions-in-c), also more info on CRTP [here](https://stackoverflow.com/questions/4173254/what-is-the-curiously-recurring-template-pattern-crtp). Fantastic answer thanks! – georgeman93 Oct 06 '21 at 10:10