MadMonkey MadMonkey - 3 months ago 10
C++ Question

Setting size of custom C++ container as template parameter vs constructor

I've written a fixed-size container (a ring buffer, to be exact) in C++. Currently I'm setting the size of the container in the constructor and then allocate the actual buffer on the heap. However, I've been thinking about moving the size parameter out of the constructor and into the template.

Going from this (RingBuffer fitting 100 integers)

RingBuffer<int> buffer(size);


to this

RingBuffer<int, 100> buffer;


This would allow me to allocate the whole buffer on the stack, which is faster than heap allocation, as far as I know. Mainly it's a matter of readability and maintainability though. These buffers often appear as members of classes. I have to initialize them with a size, so I have to initialize them in the initializer-list of every single constructor of the class. That means if I want to change the capacity of the RingBuffer I have to either remember to change it in every initializer-list or work with awkward
static const int BUFFER_SIZE = 100;
member variables.

My question is, is there any downside to specifying the container size as a template parameter as opposed to in the constructor? What are the pros and cons of either method?

As far as I know the compiler will generate a new type for each differently-sized RingBuffer. This could turn out to be quite a few. Does that hurt compile times much? Does it bloat the code or prevent optimizations? Of course I'm aware that much of this depends on the exact use case but what are the things I need to be aware of when making this decision?

Answer

My question is, is there any downside to specifying the container size as a template parameter as opposed to in the constructor? What are the pros and cons of either method?

If you give the size as template parameter, then it needs to be a constexpr (compile time constant expression). Thus your buffer size cannot depend on any run time characteristics (like user input).

Being a compile time constant opens up doors for some optimizations (loop unrolling and constant folding come to my mind) to be more efficient.

As far as I know the compiler will generate a new type for each differently-sized RingBuffer.

This is true. But I wouldn't worry about that, as having many different types per se won't have any impact on performance or code size (but probably on compile time).

Does that hurt compile times much?

It will make compilation slower. Though I doubt that in your case (this is a pretty simple template) this will even be noticeable. Thus it depends on your definition of "much".

Does it bloat the code or prevent optimizations?

Prevent optimizations? No. Bloat the code? Possibly. That depends on both how exactly you implement your class and what your compiler does. Example:

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 might get compiled to (perhaps completely) unrolled loop code, and you'd have a Buffer<100>::doSomething, a Buffer<200>::doSomething and so on, each a possibly large function. doSomethingDifferently might get compiled to not much more than a simple jump instruction, so having multiple of those wouldn't be much of an issue. Though your compiler could also change doSomething to be implemented similar doSomethingDifferently, or the other way around.

So in the end:

Don't try to make this decision depend on performance, optimizations, compile time or code bloat. Decide what's more meaningful in your situation. Will there only ever be buffers with compile time known sizes?

Also:

These buffers often appear as members of classes. I have to initialize them with a size, so I have to initialize them in the initializer-list of every single constructor of the class.

Do you know "delegating constructors"?