首页 > 解决方案 > 优化 Arduino 库中 C++ 类的存储空间

问题描述

我正在编写一个 Arduino 库来包装引脚功能(、、、digitalReaddigitalWriteanalogRead。例如,我有一个作为直通的 RegularPin 类和一个反转引脚逻辑的 InvertedPin 类。当从带有 LED 的面包板连接到反转电路逻辑的继电器板时,这很有用。我只需要交换课程。我还有一个用于按钮的 DebouncedPin 类,它检查用户按下或释放的时间是否足够长,以便真正按下/释放按钮。

模拟引脚示例:

// 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 对象时使用内存,就像在 vanilla arduino C 代码中一样。我知道类的大小不能为零,但请继续阅读。接下来,我想编写一个“AveragedPin”类,它会自动多次读取选定的引脚,我想像这样堆叠我的模板类:

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;

暂时,我将嵌套引脚声明为私有成员,因为它不允许在模板声明中使用类对象。但是随后,每一层嵌套都无用地占用了堆栈上的几个字节。

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

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

下面是 FixedRangeCorrectedPin 类的代码。

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”类成员并在模板声明中替换它,template <AnalogInPin pin,int rawMin,int rawMax,int targetRange>因为这里涉及哪个引脚在编译时完全知道。

标签: c++arduino

解决方案


如您所见,我将 pin 号放在模板声明中,因为它不会在运行时更改,并且我不希望 pin 号在分配 pin 对象时使用内存,就像在 vanilla arduino C 代码中一样。

好的,如果引脚号是 Arduino 通常的编译时间常数,那么这个位就可以了。

但是,使AnalogInPin基类抽象化(即添加virtual方法)实际上将使用每个对象至少与您通过不将引脚存储为整数所节省的空间一样多的空间。

细节是特定于实现的,但是运行时多态性需要某种方式来确定,对于由 指向的给定派生类对象AnalogInPin*,要调用哪个版本的虚拟方法,并且需要存储在派生类型的每个对象中。(您可以验证这是否属实,只需检查sizeof(AnalogInPin)并与没有方法sizeof的其他相同类进行比较即可。virtual

我知道类的大小不能为零,但是...

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

暂时,我将嵌套引脚声明为私有成员,因为它不允许在模板声明中使用类对象。但是随后,每一层嵌套都无用地占用了堆栈上的几个字节。

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

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,然后根本不使用PINAveragedPin<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 可以是相似的,但如果在编译时已知它们,则带有用于标志和最小/最大边界的额外模板参数。

同样,FixedRangeCorrectPin添加到您的问题中,不需要派生AnalogInPin并存储不同的引脚类型。其实它可以只继承基类

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;


编辑可变数量引脚的平均值示例,没有存储开销,假设我们将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()是一个去虚拟化的实例方法调用。

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

    它是去虚拟化的,因为虽然基类方法是virtual,但我们在编译时知道要调用的正确覆盖,因此不需要虚拟调度。

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

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


推荐阅读