c++templatesarduinodryteensy

How do I get the compiler to duplicate this code instead of me?


I'm working on a synthesizer that is controlled by MIDI messages, sometimes from multiple MIDI sources at the same time. It's based around a Teensy 4.1.

The first source I coded it to use was the USB Device MIDI -- easy: set up 8 callbacks in setup() and call usbMIDI.read() in loop().

The second source I coded it to use was the USB Host MIDI. However, I ended up with duplicate code -- all of the callback setup code and the read() call in loop().

I then added a few more USB Host MIDI source objects, created an array of pointers to the host objects, and now I iterate over the array in both setup() and loop()...yet still have a duplicate of that code for the USB Device interface.

For reference:

void setup()
{
// USB Device
    usbMIDI.setHandleNoteOff(myNoteOff);
    usbMIDI.setHandleNoteOn(myNoteOn);
    usbMIDI.setHandlePitchChange(myPitchChange);
...
// USB Host
    for(uint8_t i = 0; i < CNT_MIDI; i++)
    {
        midiDevices[i]->setHandleNoteOff(myNoteOff);
        midiDevices[i]->setHandleNoteOn(myNoteOn);
        midiDevices[i]->setHandlePitchChange(myPitchChange);
...
    }
}
...
void loop()
{
    usbMIDI.read();

    for(uint8_t i = 0; i < CNT_MIDI; i++)
       midiDevices[i]->read();
...
}

The interfaces of the two classes (USBdevice: usb_midi_class, USBhost: MIDIDeviceBase) are the same for these calls (read(), setHandle...(function pointer)), but since they have no common base class, I can't just add the USB Device MIDI object to the array of USB Host pointers.

Now, I want to add a 5-pin DIN MIDI port via one of the Teensy's hardware serial ports and would have three copies of the same calls...and I'm wondering if there's a better way.

My goal is to call the same-named member functions on all three of these types without writing those calls out three times. It doesn't bother me if the compiler does it. I'm not going to add or remove objects after setup() either; I know what the sources will be at compile time.

This feels like a job for templates, but it's not obvious to me how I would accomplish this without duplicating all of the code in yet-another way.

For example, I think I could write a common base class, 9 virtual functions, and create a derived template class to inherit from it, then define those 9 virtual functions like:

class Wrapper
{
    public:
    virtual bool read(uint8_t channel) = 0;
    virtual ~Wrapper() = 0;
...
};


template <class T>
class TemplateWrapper : public Wrapper
{
    private:
        T* wrapped;
    public:
    TemplateWrapper(T* w) : wrapped(w) {}
    bool read(uint8_t channel) { return wrapped->read(channel); }
...
};

But it still makes me rewrite the code several times, just in different places. Also, I have to write out 8 signatures with function pointers.

Is there a better way?


Solution

  • Using templates is a good idea but you don't need to actually create a base class for each object. You can just create a function witch accepts a reference to some templated argument and call your functions on it.

    template <typename T>
    void setupDevice(T& device) {
        device.setHandleNoteOff(myNoteOff);
        device.setHandleNoteOn(myNoteOn);
        device.setHandlePitchChange(myPitchChange);
    }
    

    You can additionally overload that function to accept pointers instead of references as well since you are using both in your shown code.

    template <typename T>
    void setupDevice(T* device) {
        device->setHandleNoteOff(myNoteOff);
        device->setHandleNoteOn(myNoteOn);
        device->setHandlePitchChange(myPitchChange);
    }
    

    If your compiler supports C++17 and above you could use fold expressions to pass all devices to your function in one go too.

    template <typename... Args>
    void setupDevice(Args... args) {
        (args.setHandleNoteOff(myNoteOff), ...);
        (args.setHandleNoteOn(myNoteOn), ...);
        (args.setHandlePitchChange(myPitchChange), ...);
    }
    

    As @Yksisarvinen noted this would pass the same parameters to all .setHandle...() methods, so you could of course change my setupDevice() function to accept those additional parameters like the following (I simply assumed int as their type but change them to your needs):

    template <typename T>
    void setupDevice(T& device, int myNoteOff, int myNoteOn, int myPitchChange) {
        device.setHandleNoteOff(myNoteOff);
        device.setHandleNoteOn(myNoteOn);
        device.setHandlePitchChange(myPitchChange);
    }