如何在没有 运行 时间开销的情况下轻松配置 类?

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

我最近开始玩 Arduinos,来自 Java 世界,我正在努力应对微控制器编程的限制。我越来越接近 Arduino 2 KB RAM 限制。

我经常面临的一个难题是如何在不增加其编译大小的情况下使代码更具可重用性和可重新配置性,尤其是当它仅用于特定构建中的一种特定配置时。

例如7-segment number displays 的通用驱动程序 class 至少需要为每个 I/O 引脚号配置LED 段,使 class 可用于不同的电路:

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

我在这里为灵活性付出的代价是额外字段的 8 个额外 RAM 字节,以及每次 class 访问这些字段时更多的代码字节和开销。但是在任何特定电路上对这个 class 的任何特定编译期间, 这个 class 只用一组值 实例化,并且这些值在被初始化之前被初始化读。它们实际上是恒定的,就像我写的一样:

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

不幸的是,GCC 不同意这种理解。

我考虑过使用“模板”:

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;

对于这个简化的例子,其中特定参数是同类的,并且总是指定的,这不是太麻烦。但我想要更多参数:例如,我还需要能够为显示器的数字配置公共引脚的数量(对于可配置的数字数量),并配置 LED 极性:共阳极或共阴极。也许将来会有更多选择。将其塞入模板初始化行会变得很丑陋。而且这个问题还不止这个class: 我处处掉进这个裂痕

我想让我的代码可配置、可重用、美观,但每次我向某些东西添加可配置字段时,它都会占用更多 RAM 字节才能恢复到相同级别的功能。

看着空闲内存的数量逐渐减少,感觉就像是因为写代码而受到惩罚,这并不好玩。

感觉自己少了点技巧


我已经为这个问题增加了悬赏,因为虽然我非常喜欢@alterigel 展示的模板配置结构,但我不喜欢它强制重新指定每个字段的精确类型,这是冗长和感觉很脆。它对数组特别讨厌(加上一些 Arduino 限制,例如不支持 constexpr inlinestd::array,显然)。

配置结构最终几乎完全由结构样板组成,而不是我理想中的样子:只是键和值的简明描述。

我一定是因为不了解 C++ 而错过了一些替代方案。更多模板?宏?遗产?内联技巧?为了避免这个问题变得过于宽泛,我特别感兴趣的是 零 运行 时间开销 .

的方法

编辑: 我已经从这里删除了示例代码的其余部分。我把它包括在内是为了避免被“过于宽泛”的警察关闭,但它似乎会分散人们的注意力。我的问题与 7 段显示器无关,甚至与 Arduinos 无关。 我只想知道在 C++ 中配置 class 行为的方法,在编译时具有零 运行 时间开销。

您可以使用单个 struct 将这些常量封装为命名静态常量,而不是单独的模板参数。然后,您可以将此 struct 类型作为单个模板参数传递,模板可以按名称查找每个常量。例如:

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

您的模板可以期望任何类型提供所需字段作为结构范围内的命名静态变量。

示例模板实现:

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

以及用法示例:

auto display = SevenSeg<TodaysConfig>{};

Live Example

这对整个问题没有任何帮助,但改善了 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);
    }
}

这必须是 if constexpr 才能正常工作的模板。

如果我对您的情况的理解正确,那么无论何时编译您的程序,您都会针对一个特定的 architecture/device 和一个特定的设置。从来没有你的程序会同时处理多个设置的情况,对吗? 我还假设你的整个项目最终比较小。

如果是这样的话,我可能会放弃任何花哨的模板或 objects。相反,对于您希望为其编译的每个设备,创建一个单独的 header 文件,其中所有设置都作为全局 constexpr 常量或枚举给出。如果更改目标,则需要提供不同的配置 header 文件并重新编译整个程序。

唯一缺少的部分是如何使您的程序包含适当的配置header?这可以通过预处理器解决:根据所需的设备,您可以在调用编译器时传递不同的命令行 -D<setting_identification_macro>。然后,创建一个 header 文件作为选择器。在那里你以

的形式列出所有支持的设备
#ifdef setting_identification_macro
#include "corresponding_config.h"
#endif

您可能会对这个“hacky”解决方案感到畏缩,但它有很多优点:

  • 没有 run-time 所需的开销
  • 绝对没有样板代码。没有要传递的结构或模板参数。
  • 在设置之间切换时无需更改代码。您只需在调用编译器时更改命令行参数即可。
  • 可以在 old/limited C++ 或普通 C 中完成