pshunter
pshunter

Reputation: 71

Optimizing storage space for classes in C++ in an Arduino library

I'm writing an Arduino library to wrap the pin functions (digitalRead, digitalWrite, analogRead, etc.). For example, I have a RegularPin class which is a passthrough and an InvertedPin class which inverts the pin logic. This is useful when going from the breadbord with LEDs to a relay board which inverts the circuit logic. I just have to swap classes. I also have a DebouncedPin class for buttons which checks the user presses or releases long enough for the button to be really pressed/released.

Example for analog pins:

// AnalogInPin ------------------------------
class AnalogInPin
{
  public:
    virtual int read()=0;
    virtual int getNo()=0;
};

// AnalogRegInPin ---------------------------

template<int pinNo>
class AnalogRegInPin : public AnalogInPin
{
  public:
    AnalogRegInPin();
    int read();
    int getNo(){return pinNo;}
};

template<int pinNo>
int AnalogRegInPin<pinNo>::read()
{
    return analogRead(pinNo);
}

template<int pinNo>
AnalogRegInPin<pinNo>::AnalogRegInPin()
{
    pinMode(pinNo, INPUT);
}

As you can see, I put the pin number in the template declaration because it is not to be changed at run time and I do not want the pin number to use memory when I allocate a pin object, just like in vanilla arduino C code. I know classes can not be of size zero but read on. Next, I want to write an "AveragedPin" class which will automatically read the selected pin several times and I would like to stack my templated classes like this :

AveragedPin<cAnalogRegInPin<A0>, UPDATE_ON_READ|RESET_ON_READ> ava0;

or even :

RangeCorrectedPin<AveragedPin<cAnalogRegInPin<A0>, 
    UPDATE_ON_READ|RESET_ON_READ,RAW_MIN,RAW_MAX,TARGET_RANGE> rcava0;

For the time being, I declared the nested pin as a private member because it is not allowed to use a class object in the template declaration. But then, each layer of nesting uselessly eats several bytes on the stack.

I know I could use references in the template declaration, but I don't quite understand how that works/should be used. My problem looks like empty member optimization, but it doesn't seem to apply here.

I feel this is more a C++ question than an arduino one and I'm not a C++ expert. I guess this touches the more advanced parts of C++. Maybe what I want is not possible, or only with recent C++ (20?) revisions.

Below is the code for the FixedRangeCorrectedPin class.

template <class P, int rawMin, int rawMax, int targetRange>
class FixedRangeCorrectedPin : public AnalogInPin
{
  public:
    int read();
    int getNo(){return pin.getNo();}
  private:
    P pin;
};

template <class P, int rawMin, int rawMax, int targetRange>
int FixedRangeCorrectedPin<P, rawMin, rawMax, targetRange>::read()
{
    int rawRange = rawMax - rawMin;
    long int result = pin.read() - rawMin;
    if (result < 0) result = 0;
    result = result * targetRange / rawRange;
    if (result > targetRange) result = targetRange;
    return result;
}

My problem is that I would like to remove the 'P pin' class member and replace it in the template declaration like in template <AnalogInPin pin,int rawMin,int rawMax,int targetRange> because which pin is involved here is completely known at compile time.

Upvotes: 1

Views: 278

Answers (1)

Useless
Useless

Reputation: 67852

As you can see, I put the pin number in the template declaration because it is not to be changed at run time and I do not want the pin number to use memory when I allocate a pin object, just like in vanilla arduino C code.

OK, if the pin number is a compile-time constant as it usually is for Arduino, this bit is fine.

However, making the AnalogInPin base class abstract (ie, adding virtual methods) will in practice use at least as much space per object as you saved by not storing the pin as an integer.

The details are implementation-specific, but runtime polymorphism requires some way of figuring out, for a given derived-class object pointed to by an AnalogInPin*, which version of the virtual methods to call, and that requires storage in each object of derived type. (You can verify that this is true buy just checking sizeof(AnalogInPin) and comparing to sizeof an otherwise identical class with no virtual methods.

I know classes can not be of size zero but ...

There's an special case for base classes with no data members that allows them to take no extra size (an instance of the most-derived type must still occupy at least one byte). It's called the empty base class optimization.

For the time being, I declared the nested pin as a private member because it is not allowed to use a class object in the template declaration. But then, each layer of nesting uselessly eats several bytes on the stack.

We can flatten the whole thing (and ideally remove the abstract base too, unless you have non-templated code that needs it):

template <int PIN, template <int> class BASE>
struct AveragedPin: public BASE<PIN>
{
    int read() override { /* call BASE<PIN>::read() several times */ }
    int getNo() override { return PIN; }
};

However, note that we could just use the inherited getNo, and then don't really use PIN at all. So instead of declaring an averaged pin instance as AveragedPin<MY_PIN, AnalogInPin> myAveragedPin;, we could change the definition to

template <class BASE>
struct AveragedPin: public BASE
{
    int read() override { /* call BASE::read() several times */ }
    using BASE::getNo; // not really required unless it is hidden
};

and declare an instance as AveragedPin<AnalogInPin<MY_PIN>> myAveragedPin;.

The range-corrected pin can be similar but with extra template parameters for the flags and min/max bounds, if they're known at compile time.

Similarly, the FixedRangeCorrectPin added to your question, doesn't need to derive from AnalogInPin and then also store a different pin type. In fact, it can just inherit the base class

template <class P,int rawMin,int rawMax,int targetRange>
struct FixedRangeCorrectedPin : public P
{
    int read(); // calls P::read()
    // inherit getNo again
};

again, declaring an instance like FixedRangeCorrectPin<AnalogInPin<MY_PIN>, RMIN, RMAX, TARGET> myFixedPin;


Edit Example of an average over a variable number of pins, with no storage overhead, assuming we changed the virtual methods to static:

template <class... PINS>
struct AveragedPins
{
  static int read()
  {
    return (PINS::read() + ...) / sizeof...(PINS);
  }
};

This doesn't care what sort of pin the argument is, so long as it has a static read method. You can stack it however you like:

using a1 = FixedRangeCorrectedPin<A_1, 0, 255, 128>;
using a2 = AnalogInPin<A_2>;
using a3 = AnalogInPin<A_3>;
using a4 = AnalogInPin<A_4>;
using a34 = AveragedPins<a3, a4>;
using all = AveragedPins<a1, a2, a34>;

// now a34::read() = (a3::read() + a4::read())/2
// and all::read() = (a1::read() + a2::read() + a34::read())/3

and note that all of those are just type definitions: we're not allocating even one byte for any objects.


One more note: I noticed that I'm using the same CLASS::method() syntax in two slightly different ways.

  1. in the first examples above, which use inheritance, BASE::read() is a de-virtualized instance method call.

    That is, we're calling BASE's version of the read method on this object. You could also write this->BASE::read().

    It's de-virtualized because although the base-class method is virtual, we know at compile time the right override to call, so virtual dispatch isn't necessary.

  2. in the final examples, where we stopped using inheritance and made the methods static, PIN::read() has no this and there is no object at all.

    This is the most similar in principle to calling a free C function, although we're getting the compiler to generate a new instance of it for each different PIN value (and then expecting it to inline the call anyway).

Upvotes: 1

Related Questions