如何让编译器代替我复制此代码?

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

提问人:Christopher Dickey 提问时间:9/28/2023 最后编辑:YksisarvinenChristopher Dickey 更新时间:9/28/2023 访问量:94

问:

我正在开发一个由MIDI消息控制的合成器,有时同时来自多个MIDI源。它基于 Teensy 4.1。

我编码它使用的第一个源是 USB 设备 MIDI——很简单:在 setup() 中设置 8 个回调并在 loop() 中调用 usbMIDI.read()。

我编码它使用的第二个来源是 USB Host MIDI。但是,我最终得到了重复的代码 - 所有回调设置代码和 loop() 中的 read() 调用。

然后,我又添加了几个 USB 主机 MIDI 源对象,创建了一个指向主机对象的指针数组,现在我在 setup() 和 loop() 中迭代该数组......但仍有 USB 设备接口的该代码的副本。

供参考:

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();
...
}

usb_midi_class对于这些调用(read()、setHandle...(函数指针)),但由于它们没有通用的基类,因此我不能将 USB 设备 MIDI 对象添加到 USB 主机指针数组中。

现在,我想通过 Teensy 的一个硬件串行端口添加一个 5 针 DIN MIDI 端口,并且将有三个相同调用的副本......我想知道是否有更好的方法。

我的目标是在所有这三种类型上调用同名的成员函数,而无需将这些调用写出三次。如果编译器这样做,我不会打扰。我也不打算在setup()之后添加或删除对象;我知道编译时的来源是什么。

这感觉像是模板的工作,但对我来说,如果不以另一种方式复制所有代码,我将如何完成这项工作并不明显。

例如,我想我可以编写一个通用的基类、9 个虚拟函数,并创建一个派生的模板类来继承它,然后定义这 9 个虚拟函数,例如:

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); }
...
};

但它仍然让我重写了几次代码,只是在不同的地方。另外,我必须用函数指针写出 8 个签名。

有没有更好的方法?

C++ 模板 Arduino Dry Teensy

评论

2赞 Yksisarvinen 9/28/2023
~你的目标是哪个 C++ 版本?~ 好吧,我看到这是一个 Arduino 库,所以恐怕没有带有 std::variant 的花哨解决方案。

答:

1赞 Joel 9/28/2023 #1

使用模板是个好主意,但您不需要为每个对象实际创建基类。你可以创建一个函数 witch 接受对某个模板化参数的引用,并在其上调用你的函数。

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

您还可以重载该函数以接受指针而不是引用,因为您在显示的代码中同时使用两者。

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

如果您的编译器支持 C++17 及更高版本,您也可以使用折叠表达式一次性将所有设备传递给您的函数。

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

正如@Yksisarvinen所指出的,这会将相同的参数传递给所有方法,因此您当然可以更改我的函数以接受这些附加参数,如下所示(我只是假设它们的类型,但根据您的需要更改它们):.setHandle...()setupDevice()int

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

评论

0赞 Yksisarvinen 9/28/2023
这是一个很好的答案,假设所有函数都需要使用完全相同的参数调用
0赞 Joel 9/28/2023
@Yksisarvinen 您也可以使用固定类型或模板指定参数。我没有包括它们,因为我不知道它们的类型,但这没问题。从他的例子来看,我认为他为他们使用了相同的参数。
0赞 Yksisarvinen 9/28/2023
我只是想指出这一点。如果设备需要不同的参数,则最终可以接近原始代码(只是使用非常长的参数列表而不是很长的调用序列)。尽管如此,这是一个很好的解决方案,并且可能是 OP 问题的答案。