如何在没有运行时开销的情况下轻松配置类?
我最近开始使用 Arduinos,并且来自 Java 世界,我正在努力应对微控制器编程的限制。我越来越接近 Arduino 2 KB RAM 的限制。
我经常面临的一个难题是如何使代码更具可重用性和可重新配置性,而不增加其编译大小,尤其是当它仅用于特定构建中的一种特定配置时。
例如,用于7 段数字显示器的通用驱动程序类至少需要配置每个 LED 段的 I/O 引脚编号,以使该类可用于不同的电路:
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 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 极性:共阳极或共阴极。也许未来会有更多选择。把它塞进模板初始化行会很丑。而这个问题不仅限于这一类:我到处都陷入这种裂痕。
我想让我的代码可配置、可重用、美观,但每次我向某些内容添加可配置字段时,它都会占用更多 RAM 字节,只是为了恢复到相同级别的功能。
看着空闲内存数量逐渐减少感觉就像是因为编写代码而受到惩罚,这并不好玩。
我觉得我错过了一些技巧。
我为这个问题增加了悬赏,因为虽然我非常喜欢 @alterigel 显示的模板配置结构,但我不喜欢它强制重新指定每个字段的精确类型,这很冗长而且感觉很脆弱。它特别讨厌数组(由于一些 Arduino 限制,例如不支持constexpr inline或std::array,显然)。
配置结构最终几乎完全由结构样板组成,而不是我理想中的样子:只是键和值的简明描述。
由于不了解 C++,我一定错过了一些替代方案。更多模板?宏?遗产?内联技巧?为了避免这个问题变得过于宽泛,我对零运行时开销的方法特别感兴趣。
编辑:我已经从这里删除了其余的示例代码。我包括它以避免被“过于广泛”的警察关闭,但它似乎分散了人们的注意力。我的问题与 7 段显示器无关,甚至与 Arduinos 无关。我只想知道 C++ 中在编译时配置类行为的方法,这些方法的运行时开销为零。
回答
您可以使用单个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>{};
现场示例
- @Boann: I'm a big fan of using `static_assert` very aggressively, especially with anything involving templates.