1

I have been developing a C++ software driver for the adc peripheral of the MCU.

The individual analog inputs connected to the adc can be configured for operation in the unipolar or bipolar mode. To reflect this fact in my design I have decided to model the analog inputs by the AnalogInput abstract class and then define two derived classes. UnipolarAnalogInput for the unipolar analog inputs and BipolarAnalogInput for the bipolar analog inputs. These two classes differ only in the implementation of the getValue() method.

enum class Type
{
Unipolar,
Bipolar
};

class AnalogInput
{
public:

virtual float getValue() = 0; 

};

class UnipolarAnalogInput : public AnalogInput
{
public:

UnipolarAnalogInput(uint8_t _id, bool _enabled, Type _type);
bool isEnabled();
bool isReady();
float getValue(); 

private:

uint8_t id;
Type type;
bool enabled;
bool ready;
uint16_t raw_value;

};

class BipolarAnalogInput : public AnalogInput
{
public:

BipolarAnalogInput(uint8_t _id, bool _enabled, Type _type);
bool isEnabled();
bool isReady();
float getValue(); 

private:

uint8_t id;
Type type;
bool enabled;
bool ready;
uint16_t raw_value;

};

My goal is to fullfill following requirements:

  1. work with both types of the analog inputs uniformly
  2. have a chance to create either the instance of the UnipolarAnalogInput or BipolarAnalogInput based on users configuration of the Adc which is known at the compile time
  3. have a chance to create the instances in for loop iteration
  4. have the implementation which is suitable for the embedded systems

Here are my ideas

As far as the requirement 1.

The ideal state would be to have AnalogInput analog_inputs[NO_ANALOG_INPUTS]. As far as I understand correctly this is not possible in C++. Instead of that I need to define AnalogInput *analog_inputs[NO_ANALOG_INPUTS].

As far as the requirement 2.

It seems to me that the best solution for the other systems than the embedded systems would be to use the factory method design pattern i.e. inside the AnalogInput define

static AnalogInput* getInstance(Type type) {
    if(type == Unipolar) {
        // create instance of the UnipolarAnalogInput
    } else if(type == Bipolar) {
        // create instance of the BipolarAnalogInput
    }
}

Here I would probably need to define somewhere auxiliary arrays for the UnipolarAnalogInput instances and the BipolarAnalogInput instances where the instances would be allocated by the factory method and the pointers to those arrays would be returned by the getInstance(). This solution seems to me to be pretty cumbersome due to the auxiliary arrays presence.

As far as the requirement 3.

for(uint8_t input = 0; input < NO_ANALOG_INPUTS; input++) {
    analog_inputs[input] = AnalogInput::getInstance(AdcConfig->getInputType(input));
}

As far as the requirement 4.

Here I would say that what I have suggested above is applicable also for the embedded systems because the solution avoids usage of the standard new operator. Question mark is the virtual method getValue().

My questions is whether the auxiliary arrays presence is unavoidable?

Steve
  • 805
  • 7
  • 27
  • You might be interested in looking up [_static polymorphism_](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern). HAL configutation should be made during compile time, there's no need for virtual polymorphism usually. Also here at SO: https://stackoverflow.com/questions/4173254/what-is-the-curiously-recurring-template-pattern-crtp – πάντα ῥεῖ Jan 25 '21 at 16:23
  • 2
    "_what do you think about ...?_" is inviting opinion (as is pretty much the rest of the question), so off-topic. Q3 perhaps has some merit, but you could ask that with a much shorter question (that I might bother to read, because I am not working that hard for free ;-) ). I think "exploit" is probably the wrong word; it suggests using something in a way considered as not its intended use - and you are not doing that, its just polymorphism. – Clifford Jan 25 '21 at 16:32
  • 1
    I am not (yet) posting an answer because it is currently too broad IMO, invites opinion and lacks focus. w.r,t.Q3. however rather than two separate pools (arrays or whatever) of each variant, you could have a single memory block and instantiate any object in it using placement new. – Clifford Jan 25 '21 at 16:38
  • What is the actual problem you are trying to solve? To me, the problem isn't "how do I implement two different behaviors of my driver" but rather "how do I create something that fits my factory pattern". You need to ask yourself how a factory pattern makes sense inside a low level hardware driver to begin with. How do you handle re-entrancy between ADC reads and the caller, for example. Is the ADC polled, interrupt-based or DMA? That's very fundamental stuff which is not even addressed by this code. – Lundin Jan 26 '21 at 09:18
  • One of many reasons C++ should be avoided in general, but for embedded in particular, is because it tends to frequently send you down a trap door to meta programming hell. If you find yourself programming various "adapters" or interfaces to fit other interfaces in your code, then you have likely ended up there. The only way to get out of it is to strip down all abstraction layers until you are only left with one that actually makes sense for the application. These design aspects are not easy to get right, it's very qualified work. And C++ tends to blow your whole leg off when you get it wrong. – Lundin Jan 26 '21 at 09:23
  • @πάνταῥεῖ thank you for the idea with the static polymorphism. I have studied the link you have mentioned. Correct me if I am wrong but my understanding is that neither the CRTP enables to have a container containing at once instances of various polymorphic classes which was my intention. – Steve Feb 07 '21 at 12:36
  • @Steve You can still use static polymorphism with raw c-style opaque pointers internally, if needed. Generally you can use that pattern, if all type variants are known at compile time (hence the _static_). How you cast pointer arrays to their right underlying types is mainly your responsibility, but automatable with some template metaprogramming trickery. – πάντα ῥεῖ Feb 07 '21 at 12:44

1 Answers1

1

The "auxiliary array" as you call it is mostly needed for memory management, i.e. you need to choose the memory to store your objects in. It's also an interface - the array is how you access the ADCs.

You can store your objects either in the heap or the (global) data segment - an array of objects implements the latter (you can also create global variables, one per ADC, which is a worse solution). If the compiler has all the information it needs to allocate the memory during compilation, it's usually the preferred approach. However - as you've noticed - polymorphism becomes rather annoying to implement with statically allocated objects.

The alternative is to keep them in heap. This is often totally acceptable in an embedded system if you allocate the heap memory at startup and keep it permanently (i.e. never try to release or re-use this part of heap, which would risk fragmentation). And this is really the only humane way to do polymorphic stuff, especially object instantiation.

If you don't like the array, use some other storage method - linked list, global variables, whatever. But you need to access the objects through a pointer (or a reference, which is also a pointer) for polymorhpism to work. And arrays are a simple concept, so why not use them?

Tarmo
  • 3,728
  • 1
  • 8
  • 25