【问题标题】:Setting size of custom C++ container as template parameter vs constructor将自定义 C++ 容器的大小设置为模板参数与构造函数
【发布时间】:2016-09-06 20:22:57
【问题描述】:

我已经用 C++ 编写了一个固定大小的容器(准确地说是环形缓冲区)。目前我在构造函数中设置容器的大小,然后在堆上分配实际的缓冲区。但是,我一直在考虑将 size 参数从构造函数中移到模板中。

从此开始(RingBuffer 拟合 100 个整数)

RingBuffer<int> buffer(size);

到这里

RingBuffer<int, 100> buffer;

据我所知,这将允许我在堆栈上分配整个缓冲区,这比堆分配要快。主要是可读性和可维护性的问题。这些缓冲区通常作为类的成员出现。我必须用一个大小来初始化它们,所以我必须在类的每个构造函数的初始化列表中初始化它们。这意味着如果我想更改 RingBuffer 的容量,我必须记住在每个初始化器列表中更改它,或者使用尴尬的 static const int BUFFER_SIZE = 100; 成员变量。

我的问题是,与在构造函数中相比,将容器大小指定为模板参数有什么缺点吗?两种方法的优缺点是什么?

据我所知,编译器将为每个不同大小的 RingBuffer 生成一个新类型。这可能是相当多的。这对编译时间有很大影响吗?它会膨胀代码还是阻止优化?当然,我知道这在很大程度上取决于具体的用例,但在做出此决定时我需要注意哪些事项?

【问题讨论】:

  • 是的,会有膨胀。今天的数兆字节“你好世界!”不会引起明显的膨胀。应用标准。优化可能会更好,但 CPU 缓存利用率可能会受到影响。我怀疑除了尝试和亲眼看到你得到什么之外,是否可以通过任何方式给出明确的答案。 (但如果你这样做了,请务必发布你的结果。)
  • (然后没有人会把你的结果当作确定的事实,并且可能会想自己重复实验。)
  • 作为模板参数,不同大小的容器会是不同的类型。这可能是一个优点或一个缺点,这取决于你想如何使用它。

标签: c++ templates containers


【解决方案1】:

我的问题是,与在构造函数中相比,将容器大小指定为模板参数有什么缺点吗?两种方法的优缺点是什么?

如果您将size 作为模板参数,那么它必须是constexpr(编译时常量表达式)。因此,您的缓冲区大小不能取决于任何运行时特征(如用户输入)。

作为编译时常量为一些优化打开了大门(我想到循环展开和常量折叠)以提高效率。

据我所知,编译器会为每个不同大小的 RingBuffer 生成一个新类型。

这是真的。但我不会担心这一点,因为拥有许多不同的类型本身不会对性能或代码大小产生任何影响(但可能会影响编译时间)。

这对编译时间有很大影响吗?

它会使编译变慢。尽管我怀疑在您的情况下(这是一个非常简单的模板),这甚至会很明显。因此,这取决于您对“很多”的定义。

它会使代码膨胀还是阻止优化?

阻止优化?不,膨胀代码?可能。这取决于你如何准确地实现你的类和你的编译器做什么。示例:

template<size_t N>
struct Buffer {
  std::array<char, N> data;

  void doSomething(std::function<void(char)> f) {
    for (size_t i = 0; i < N; ++i) {
       f(data[i]);
    }
  }
  void doSomethingDifferently(std::function<void(char)> f) {
    doIt(data.data(), N, f);
  }
};

void doIt(char const * data, size_t size, std::function<void(char)> f) {
  for (size_t i = 0; i < size; ++i) {
    f(data[i]);
  }
}

doSomething 可能会被编译为(可能完全)展开的循环代码,并且您将拥有一个 Buffer&lt;100&gt;::doSomething、一个 Buffer&lt;200&gt;::doSomething 等等,每个都可能是一个很大的函数。 doSomethingDifferently 可能会被编译成一个简单的跳转指令,因此拥有多个这样的指令并不是什么大问题。尽管您的编译器也可以将doSomething 更改为类似doSomethingDifferently 的实现,或者反过来。

所以最后:

不要试图根据性能、优化、编译时间或代码膨胀来做出这个决定。 决定在你的情况下什么更有意义。是否只有编译时间已知大小的缓冲区?

还有:

这些缓冲区通常作为类的成员出现。我必须用一个大小来初始化它们,所以我必须在类的每个构造函数的初始化列表中初始化它们。

你知道“委托构造函数”吗?

【讨论】:

    【解决方案2】:

    正如 Daniel Jour 已经说过的那样,代码膨胀并不是一个大问题,如果需要可以处理。 将大小设置为constexpr 的好处在于,它可以让您在编译时检测到一些错误,否则这些错误会在运行时发生。

    据我所知,这将允许我在堆栈上分配整个缓冲区,这比堆分配要快。 这些缓冲区通常作为类的成员出现

    只有在自动内存中分配拥有类时才会发生这种情况。通常情况并非如此。考虑以下示例:

    struct A {
        int myArray[10];
    };
    
    struct B {
        B(): dynamic(new A()) {}
    
        A automatic; // should be in the "stack"
        A* dynamic; // should be in the "heap"
    };
    
    int main() {
        B b1;
        b1;                         // automatic memory
        b1.automatic;               // automatic memory
        b1.automatic.myArray;       // automatic memory
        b1.dynamic;                 // automatic memory
        (*b1.dynamic);              // dynamic memory
        (*b1.dynamic).myArray;      // dynamic memory
    
        B* b2 = new B();
        b2;                         // automatic memory
        (*b2);                      // dynamic memory
        (*b2).automatic;            // dynamic memory
        (*b2).automatic.myArray;    // dynamic memory
        (*b2).dynamic;              // dynamic memory
        (*(*b2).dynamic).myArray;   // dynamic memory
    }
    

    【讨论】:

      猜你喜欢
      • 2023-02-24
      • 2010-12-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-18
      • 1970-01-01
      • 1970-01-01
      • 2023-01-30
      相关资源
      最近更新 更多