在 Arduino 库中用 C++ 为 类 优化存储 space

Optimizing storage space for classes in C++ in an Arduino library

我正在编写一个 Arduino 库来包装引脚功能(digitalReaddigitalWriteanalogRead 等)。例如,我有一个 RegularPin class 是直通,还有一个 InvertedPin class 反转引脚逻辑。当从带有 LED 的面包板到反转电路逻辑的继电器板时,这很有用。我只需要交换 classes。 我还有一个用于按钮的 DebouncedPin class,它检查用户按下或释放的时间是否足以使按钮真正 pressed/released.

模拟引脚示例:

// AnalogInPin ------------------------------
class AnalogInPin
{
  public:
    virtual int read()=0;
    virtual int getNo()=0;
};

// AnalogRegInPin ---------------------------

template<int pinNo>
class AnalogRegInPin : public AnalogInPin
{
  public:
    AnalogRegInPin();
    int read();
    int getNo(){return pinNo;}
};

template<int pinNo>
int AnalogRegInPin<pinNo>::read()
{
    return analogRead(pinNo);
}

template<int pinNo>
AnalogRegInPin<pinNo>::AnalogRegInPin()
{
    pinMode(pinNo, INPUT);
}

如您所见,我将 pin 号放在模板声明中,因为它不会在 运行 时更改,并且我不希望 pin 号在分配 pin 对象时占用内存,就像在香草 arduino C 代码中一样。我知道 classes 的大小不能为零,但请继续阅读。接下来,我想编写一个“AveragedPin”class,它将自动多次读取选定的引脚,我想像这样堆叠我的模板化 classes :

AveragedPin<cAnalogRegInPin<A0>, UPDATE_ON_READ|RESET_ON_READ> ava0;

甚至:

RangeCorrectedPin<AveragedPin<cAnalogRegInPin<A0>, 
    UPDATE_ON_READ|RESET_ON_READ,RAW_MIN,RAW_MAX,TARGET_RANGE> rcava0;

我暂时将嵌套的pin声明为私有成员,因为不允许在模板声明中使用class对象。但是,每一层嵌套都会无用地吃掉堆栈上的几个字节。

我知道我可以在模板声明中使用引用,但我不太明白 works/should 是如何使用的。我的问题看起来像空成员优化,但它似乎不适用于这里。

我觉得这更像是一个 C++ 问题,而不是一个 arduino 问题,而且我不是 C++ 专家。我想这涉及到 C++ 的更高级部分。也许我想要的是不可能的,或者只有最近的 C++(20?)修订。

下面是 FixedRangeCorrectedPin 的代码 class。

template <class P, int rawMin, int rawMax, int targetRange>
class FixedRangeCorrectedPin : public AnalogInPin
{
  public:
    int read();
    int getNo(){return pin.getNo();}
  private:
    P pin;
};

template <class P, int rawMin, int rawMax, int targetRange>
int FixedRangeCorrectedPin<P, rawMin, rawMax, targetRange>::read()
{
    int rawRange = rawMax - rawMin;
    long int result = pin.read() - rawMin;
    if (result < 0) result = 0;
    result = result * targetRange / rawRange;
    if (result > targetRange) result = targetRange;
    return result;
}

我的问题是我想删除 'P pin' class 成员并像 template <AnalogInPin pin,int rawMin,int rawMax,int targetRange> 那样在模板声明中替换它,因为这里涉及的引脚是完全已知的编译时间。

As you can see, I put the pin number in the template declaration because it is not to be changed at run time and I do not want the pin number to use memory when I allocate a pin object, just like in vanilla arduino C code.

好的,如果引脚号是一个编译时常量,就像 Arduino 通常那样,这个位就可以了。

但是,使 AnalogInPin 基础 class 抽象(即添加 virtual 方法)在实践中将至少使用与您保存的每个对象一样多的 space通过不将 pin 存储为整数。

细节是特定于实现的,但是运行时多态性需要一些方法来弄清楚,对于[=17]指向的给定派生-class对象=],要调用哪个版本的虚拟方法,以及需要存储在派生类型的每个对象中。 (您可以验证这是真的购买,只需检查 sizeof(AnalogInPin) 并与 sizeof 比较,其他方面相同 class,没有 virtual 方法。

I know classes can not be of size zero but ...

没有数据成员的基 classes 有一个特殊情况,允许 它们 没有 extra 大小(最派生类型的实例必须至少占用一个字节)。它被称为空基 class 优化。

For the time being, I declared the nested pin as a private member because it is not allowed to use a class object in the template declaration. But then, each layer of nesting uselessly eats several bytes on the stack.

我们可以将整个东西展平(理想情况下也可以删除抽象基础,除非您有需要它的非模板代码):

template <int PIN, template <int> class BASE>
struct AveragedPin: public BASE<PIN>
{
    int read() override { /* call BASE<PIN>::read() several times */ }
    int getNo() override { return PIN; }
};

但是请注意,我们可以只使用继承的 getNo,然后根本不使用 PIN。因此,我们可以将定义更改为

,而不是将平均引脚实例声明为 AveragedPin<MY_PIN, AnalogInPin> myAveragedPin;
template <class BASE>
struct AveragedPin: public BASE
{
    int read() override { /* call BASE::read() several times */ }
    using BASE::getNo; // not really required unless it is hidden
};

并将实例声明为 AveragedPin<AnalogInPin<MY_PIN>> myAveragedPin;

经过范围校正的 pin 可以类似,但如果在编译时已知,则带有用于标志和 min/max 边界的额外模板参数。

同样,添加到您的问题的 FixedRangeCorrectPin 不需要从 AnalogInPin 派生,然后也存储不同的 pin 类型。其实可以直接继承基class

template <class P,int rawMin,int rawMax,int targetRange>
struct FixedRangeCorrectedPin : public P
{
    int read(); // calls P::read()
    // inherit getNo again
};

再次声明一个实例,如 FixedRangeCorrectPin<AnalogInPin<MY_PIN>, RMIN, RMAX, TARGET> myFixedPin;


Edit 可变数量引脚的平均值示例,没有存储开销,假设我们将 virtual 方法更改为 static

template <class... PINS>
struct AveragedPins
{
  static int read()
  {
    return (PINS::read() + ...) / sizeof...(PINS);
  }
};

这并不关心参数是什么类型的引脚,只要它具有静态 read 方法即可。你可以随心所欲地堆叠它:

using a1 = FixedRangeCorrectedPin<A_1, 0, 255, 128>;
using a2 = AnalogInPin<A_2>;
using a3 = AnalogInPin<A_3>;
using a4 = AnalogInPin<A_4>;
using a34 = AveragedPins<a3, a4>;
using all = AveragedPins<a1, a2, a34>;

// now a34::read() = (a3::read() + a4::read())/2
// and all::read() = (a1::read() + a2::read() + a34::read())/3

请注意,所有这些都只是类型定义:我们不会为任何对象分配一个字节。


请注意:我注意到我以两种略有不同的方式使用了相同的 CLASS::method() 语法。

  1. 在上面第一个使用继承的例子中,BASE::read()是一个去虚拟化的实例方法调用。

    也就是说,我们在 this 对象上调用 BASE 版本的 read 方法。你也可以写 this->BASE::read().

    它是去虚拟化的,因为虽然 base-class 方法是 virtual,但我们知道在编译时调用正确的重写,因此不需要虚拟分派。

  2. 在最后的示例中,我们停止使用继承并将方法设为静态,PIN::read() 没有 this 并且根本没有对象。

    这在原则上与调用自由 C 函数最相似,尽管我们让编译器为每个不同的 PIN 值生成一个新实例(然后期望它内联无论如何打电话)。