【问题标题】:How to make an object take and store an Array of arbitrary, but compile-time known size?如何使对象获取并存储任意但编译时已知大小的数组?
【发布时间】:2022-01-20 14:41:48
【问题描述】:

背景

对于嵌入式项目,我想要一个带有结构列表的类。这个列表在编译时是已知的,所以我不应该为此求助于动态内存分配。

但是,如何创建一个封装此数组的结构/类,而不必将其大小用作模板参数?

模板

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

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 是存储/封装点数组(2 成员结构)的类。 n,数组的大小,是模板参数。

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

但是,没有,我有另一个使用这个 Profile 的类,现在也必须对其进行模板化,因为 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 类在Profile 上运行并对其进行迭代。必须模板化Runner 本身并不是什么大问题,但是这个Runner 又被我项目中的另一个类使用,因为这个另一个类调用Runner::foo()。现在我也必须为该类模板!以及使用该类的类等。

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

问题

有没有办法在ProfileRunner 中“隐藏”数组的大小? Runner 只需要对其进行迭代,因此大小原则上应该只影响它的实现,而不影响它的公共接口。不过,我该怎么做呢?

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

其他方法

我可以写一个类似的宏

#define n 12

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

矢量

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

【问题讨论】:

  • 您需要多少种不同的尺寸?
  • 另外,数组的这种实现似乎允许存储少于最大元素的元素(不像std::array),也许只使用您将使用的数组的最大大小就足够了?跨度>
  • 什么时候你才真正知道数组的大小?例如,如果Runner 总是需要一个Profile 和一个大小为12 的数组,那么它可以使用Profile&lt;12&gt;,如果你需要Runners 和不同大小的Profiles,那么将Runner 设为模板不会'听起来太糟糕了
  • @Yksisarvinen 还不知道。我只想要一个可以使用任意大小的Profile 类。
  • 不使用大小为n 的普通旧数组的原因是什么,其中n 是一个普通的旧整数常量?太容易了?太难读了?

标签: c++ oop embedded


【解决方案1】:

有没有办法在 Profile 或 Runner 中“隐藏”数组的大小?

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

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

间接可以在您喜欢的任何级别完成。如果您在最低级别执行此操作,您只需将数组存储在Profile 之外。事实上,如果该配置文件所做的只是包含一个数组,那么您就不需要一个类。使用通用的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. 绑定到类的数组的生命周期(安全)
  2. 没有编译时常量大小
  3. 无动态存储
  • 1,2 (no 3) = std::vector, RAII
  • 1,3 (no 2) = std::array,模板,无间接
  • 2,3 (no 1) = std::span,注意生命周期

【讨论】:

  • 谢谢,这看起来是个不错的解决方案!这是什么原因?事实上(与std::array 不同)它不会使大小成为其类型的一部分?
  • @Compizfox 它之所以有效,是因为它没有尝试将数组存储在类中。它单独存储,具有单独的生命周期。
  • 我明白了。但即使在我的方法中,我也可以放弃 Profile 作为数组周围的容器(就像你所做的那样),并且只在 Runner 中获取/存储指向数组的指针/引用。在这种情况下,它不存储数组,但它成为Runner 类型的一部分(导致完全相同的问题)。有了std::span,我就避免了,对吧?
  • @Compizfox I could ditch Profile as a container around the array (like you did) and only take/store a pointer/reference to the array in Runner 这正是我建议的解决方案(当然不是参考,因为那行不通)。 std::span 只是一个指针和一个大小的组合。
  • 我明白了,我只是在剖析为什么您的解决方案有效而我的解决方案无效;)
【解决方案2】:

我将对此评论进行扩展:

这个想法是,无论大小,Runner 都会获取配置文件。 Runner 需要对其进行迭代,但除此之外,它的行为始终相同。使用 Runner 并调用 Runner::foo() 的类不需要知道大小。模板化 Runner 的问题是使用 Runner 的类也需要模板化,使用它的类等等。

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

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

现在,如果一个类需要一个 Runner 成员,它们存储一个 std::unique_ptr&lt;RunnerInterface&gt; 并且仅在构造时您需要决定数组的大小(尽管您仍然需要在某个地方决定大小)。

【讨论】:

  • 我同意使用Runner 的班级不需要知道n。但是使用模板方法,这不可避免地会发生,对吧?模板参数“传播”。除非有一种方法可以使用类型擦除或其他方式“打破这条链”。但我不确定。我想避免运行时多态性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多