如何使对象获取并存储任意但编译时已知大小的数组?

How to make an object take and store an Array of arbitrary, but compile-time known size?

背景

对于嵌入式项目,我想要一个包含结构列表的 class。此列表在编译时已知,因此我不必为此求助于动态内存分配。

但是,如何制作一个 struct/class 来封装这个数组,而不必将其大小用作模板参数?

模板

我的第一个想法就是这样做:

struct Point {
    const uint16_t a;
    const double b;
};

template<size_t n>
struct Profile {
    Array<Point, n> points;

    Profile(const Array<Point, n> &points) : points(points) {}
};

这里,Profile 是 class 即 stores/encapsulates 点数组(2 成员结构)。 n,数组的大小,是模板参数。

我正在使用数组的 this 实现,类似于 std::array,顺便说一句,因为我无法访问此嵌入式平台上的 STL。

但是,不,我有另一个使用此 Profile 的 class 现在也必须进行模板化,因为 Profile 是使用数组大小​​进行模板化的:

template<size_t n>
class Runner {
private:
    const Profile<n> profile;
public:
    Runner(const Profile<n> &profile) : profile(profile) {};

    void foo() {
        for(auto point : profile.points) {
            // do something
        }
    }
};

可以看出,这个 Runner class 在 Profile 上运行并迭代它。必须使用模板 Runner 本身并不是什么大问题,但是这个 Runner 又被我项目中的另一个 class 使用,因为另一个 class 调用 Runner::foo()。现在我还必须为 class 设置模板! class 使用那个 class 的 es,等等

这已经失控了!一开始只有一个模板参数来指定大小,现在传播到我的整个应用程序。因此,我认为这不是一个好的解决方案。

问题

有没有办法 'hide' ProfileRunner 中数组的大小? Runner 只需要迭代它,所以原则上大小应该只影响它的实现,而不是它的 public 接口。不过,我该怎么做呢?

此外,我是否可以完全避免手动指定 n,而只是将数组传递给 Profile 的构造函数,让编译器计算出它有多大?当然是在编译时。我觉得这应该是可能的(假设这个数组在编译时是已知的),但我不知道具体如何。

其他方法

我可以写一个像这样的宏

#define n 12

并将其包含在 Profile.h 和我实例化 Profile 的地方。虽然这感觉很脏,但我想避免使用宏。

向量

我可以通过使用 std::vector(或等效的)来避免这种大惊小怪,但它是在 运行 时间分配在堆上的,我想在这里避免这种情况,因为它应该没有必要。

Is there a way to 'hide' the size of the array in Profile or Runner?

是的。解决方案是间接的。您可以指向它,而不是直接存储对象。你不需要知道你所指向的东西的大小。

一个方便的解决方案是指向动态存储(例如std::vector),因为它允许您将动态大小对象的生命周期“绑定”到一个成员。这通常不是必需的,您可以改用自动存储。但是,在那种情况下你不能绑定指向对象的生命周期,你必须非常小心不要让指向对象在你停止使用它之前被销毁。

间接可以在您喜欢的任何级别完成。如果您在最低级别执行此操作,则只需将数组存储在 Profile 之外。事实上,如果配置文件所做的只是包含一个数组,那么您就不需要 class 了。使用通用 span:

struct Runner {
    span<const Point> profile;
    void foo() {
        for(auto point : profile) {
            // do something
        }
    }
};

Point points[] {
    // ... number of points doesn't matter
};
Runner runner {
    .profile = points,
};

span,我的意思是 std::span。如果您不能使用标准库,则使用其他实现。它基本上只是一个指针和大小,带有方便的模板构造器。


澄清一下,您可以选择任意两个,但不能同时拥有这三个:

  1. 绑定到 class(安全)
  2. 的数组的生命周期
  3. 没有编译时常量大小
  4. 没有动态存储
  • 1,2(没有 3)= std::vector, RAII
  • 1,3(无 2)= std::array,模板,无间接
  • 2,3 (no 1) = std::span, 小心生命周期

我将对此评论进行扩展:

The idea is that Runner takes Profiles no matter their size. Runner needs to iterate over it, but apart from that, its behaviour is always the same. The class using Runner and calling Runner::foo() doesn't need to know the size. The problem with templating Runner is that the class using Runner also needs to be templated, and the classes using that, etc.

只有当 class 直接使用模板 Runner 时才会出现问题。它具有比实际需要更多的依赖项。如果它不需要知道数组的大小,那么它不应该知道数组的大小。如果运行时多态性是一个选项,您可以添加一个基数 class,它允许访问数组元素,但不需要知道任何关于数组大小的信息。以下仅为示意图:

#include <iostream>

struct RunnerInterface {
    virtual int* begin() = 0;
    virtual int* end() = 0;
    virtual ~RunnerInterface(){}
};

template <unsigned size>
struct Runner : RunnerInterface {
    int data[size];
    int* begin() override { return data; }
    int* end() override { return data+size; } // pointer one past the end if fine (it won't get dereferenced)
};

void foo(RunnerInterface& ri) {
    for (auto it = ri.begin(); it != ri.end(); ++it){
        *it = 42;
    }
}

void bar(RunnerInterface& ri){
    for (auto it = ri.begin(); it != ri.end(); ++it){
        std::cout << *it;
    }
}

int main() {
    Runner<42> r;
    foo(r);
    bar(r);
}

现在如果 class 需要一个 Runner 成员,他们存储一个 std::unique_ptr<RunnerInterface> 并且只有在构造时你需要决定数组的大小(尽管你仍然需要决定某处的尺寸)。