Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make classes easily configurable without run-time overhead?

I recently started playing with Arduinos, and, coming from the Java world, I am struggling to contend with the constraints of microcontroller programming. I am slipping ever closer to the Arduino 2-kilobyte RAM limit.

A puzzle I face constantly is how to make code more reusable and reconfigurable, without increasing its compiled size, especially when it is used in only one particular configuration in a particular build.

For example, a generic driver class for 7-segment number displays will need, at minimum, configuration for the I/O pin number for each LED segment, to make the class usable with different circuits:

class SevenSeg {
private:
    byte pinA; // top
    byte pinB; // upper right
    byte pinC; // lower right
    byte pinD; // bottom
    byte pinE; // lower left
    byte pinF; // upper left
    byte pinG; // middle
    byte pinDP; // decimal point
    
public:
    void setSegmentPins(byte a, byte b, byte c, byte d, byte e, byte f, byte g, byte dp) {
        /* ... init fields ... */
    }
    
    ...
};

SevenSeg display;
display.setSegmentPins(12, 10, 7, 6, 5, 9, 8, 13);
...

The price I'm paying for flexibility here is 8 extra RAM bytes for extra fields, and more code bytes and overhead every time the class accesses those fields. But during any particular compilation of this class on any particular circuit, this class is only instantiated with one set of values, and those values are initialized before ever being read. They are effectively constant, as if I had written:

class SevenSeg {
private:
    static const byte pinA = 12;
    static const byte pinB = 10;
    static const byte pinC = 7;
    static const byte pinD = 6;
    static const byte pinE = 5;
    static const byte pinF = 9;
    static const byte pinG = 8;
    static const byte pinDP = 13;
    
    ...
};

Unfortunately, GCC does not share this understanding.

I considered using a "template":

template <byte pinA, byte pinB, byte pinC, byte pinD, byte pinE, byte pinF, byte pinG, byte pinDP> class SevenSeg {
    ...
};

SevenSeg<12, 10, 7, 6, 5, 9, 8, 13> display;

For this reduced example, where the particular parameters are homogeneous, and always specified, this is not too cumbersome. But I want more parameters: For example I also need to be able to configure the numbers of the common pins for the display's digits (for a configurable amount of digits), and configure the LED polarity: common anode or common cathode. And maybe more options in the future. It will get ugly cramming that into the template initialization line. And this problem is not limited to this one class: I am falling into this rift everywhere.

I want to make my code configurable, reusable, beautiful, but every time I add configurable fields to something, it eats up more RAM bytes just to get back to the same level of functionality.

Watching the free memory number creep down feels like being punished for writing code, and that's not fun.

I feel like I'm missing some tricks.


I've added a bounty to this question because although I quite like the template config struct thing shown by @alterigel, I don't like that it forces respecification of the precise types of each field, which is verbose and feels brittle. It's particularly icky with arrays (compounded by some Arduino limitations, such as not supporting constexpr inline or std::array, apparently).

The config struct ends up consisting almost entirely of structural boilerplate, rather than what I would ideally like: just a concise description of keys and values.

I must be missing some alternatives due to not knowing C++. More templates? Macros? Inheritance? Inlining tricks? To avoid this question becoming too broad, I'm specifically interested in ways of doing this that have zero run-time overhead.


EDIT: I have removed the rest of the example code from here. I included it to avoid getting shut down by the "too broad" police, but it seemed to be distracting people. My question has nothing to do with 7-segment displays, or even Arduinos necessarily. I just want to know the ways in C++ to configure class behavior at compile time that have zero run-time overhead.

like image 293
Boann Avatar asked Mar 24 '21 21:03

Boann


3 Answers

You can use a single struct to encapsulate these constants as named static constants, rather than as individual template parameters. You can then pass this struct type as a single template parameter, and the template can expect to find each constant by name. For example:

struct YesterdaysConfig {
    static const byte pinA = 3;
    static const byte pinB = 4;
    static const byte pinC = 5;
    static const byte pinD = 6;
    static const byte pinE = 7;
    static const byte pinF = 8;
    static const byte pinG = 9;
    static const byte pinDP = 10;
};

struct TodaysConfig {
    static const byte pinA = 12;
    static const byte pinB = 10;
    static const byte pinC = 7;
    static const byte pinD = 6;
    static const byte pinE = 5;
    static const byte pinF = 9;
    static const byte pinG = 8;
    static const byte pinDP = 13;

    // Easy to extend:
    static const byte extraData = 0xFF;
    using customType = double;
};

Your template can expect any type which provides the required fields as named static variables within the struct's scope.

An example template implementation:

template<typename ConfigT>
class SevenSeg {
public:
    SevenSeg() {
        theHardware.setSegmentPins(
            ConfigT::pinA,
            ConfigT::pinB,
            ConfigT::pinC,
            ConfigT::pinD,
            ConfigT::pinE,
            ConfigT::pinF,
            ConfigT::pinG,
            ConfigT::pinDP
        );
    }
};

And an example usage:

auto display = SevenSeg<TodaysConfig>{};

Live Example

like image 103
alter_igel Avatar answered Sep 24 '22 06:09

alter_igel


If I understand your situation correctly, whenever you compile your program, you target a single, specific architecture/device with one specific setting. There is never a case where you program would deal with multiple settings at the same time, is that right? I also assume that your whole project is ultimately relatively small.

If that is the case, I would probably forgo any fancy templates or objects. Instead, for every device you desire to compile for, create a separate header file with all settings given as global constexpr constants or enums. If you change your target, you need to supply a different config header file and recompile the whole program.

The only missing component is how to make your program include appropriate config header? That can be solved with the preprocessor: Depending on the desired device, you can pass a different command line -D<setting_identification_macro> when invoking the compiler. Then, create a header file which acts as a selector. In there you list all supported devices in a form of

#ifdef setting_identification_macro
#include "corresponding_config.h"
#endif

You might cringe at this "hacky" solution, but it has many advantages:

  • No run-time overhead as you desired
  • Absolutely no boilerplate code. No structs to pass around or template arguments.
  • No change in code required when switching between settings. You just change the command line parameter when invoking the compiler.
  • Can be done in old/limited C++ or plain C
like image 33
CygnusX1 Avatar answered Sep 23 '22 06:09

CygnusX1


This does nothing for the whole problem, but improves the pgm_read:

template<class T = type>
auto pgm_read(const T *p) {
    if constexpr (std::is_same<T, float>::value) {
        return pgm_read_float(p);
    } else if constexpr (sizeof(T) == 1) {
        return pgm_read_byte(p);
    } else if constexpr (sizeof(T) == 2) {
        return pgm_read_word(p);
    } else if constexpr (sizeof(T) == 4) {
        return pgm_read_dword(p);
    }
}

This has to be a template for the if constexpr to work correctly.

like image 37
n314159 Avatar answered Sep 24 '22 06:09

n314159