C++17/20 - array-like initialization for fixed size container class

139 Views Asked by At

I have a specialized, templated container class containing a fixed size array (size defined by a template):

template<typename T, std::size_t N>
class container {

protected:
T _arr[N];
std::size_t _size = 0;      // does not necessarily equal N
\* other members *\

public:
\* ctors and member functions *\
};

Now I would like to initialize its underlying array like a c-array:

container c = { 1, 2, 3, 4 };

This line of code should initialize _arr as an integer array containing { 1, 2, 3, 4 }.

Meanwhile I want to be able to use other constructors as well. For example:

container c(size, value);     // initializes _arr until size with value

My idea of an ideal implementation would be a way to pass the size of a (constexpr) initializer list to the template argument size.

What I tried:

1. Using a constructor with an array as parameter:

container(const T (&arr)[N]) : _size(N) {
    std::copy(std::begin(arr), std::end(arr), _arr);
}

This works, when using it directly in the constructor: container c({ 1, 2, 3, 4}), but does not work when using copy construction: container c = {1, 2, 3, 4}. Using another set of curly braces around the array would fix the problem (container c = {{1, 2, 3, 4}}), but this is not what I was thinking when I first envisioned this class.

2. Using a constructor with variadic templates

template<typename ...U, typename = std::enable_if<std::conjunction_v<std::is_same<T, U>...>> >
container(U... values) : _size(sizeof...(U)) {
    T a[] = {values...};
    std::move(std::begin(a), std::end(a), _arr);
}

and a type deduction guide:

template<typename ...T>
container(T...) -> container<std::common_type_t<T...>, sizeof...(T)>;

This was the closest I got, but it unfortunately messud up the use of constructors directly (container c(size, value)).


My idea of an ideal implementation would be a way to pass the size of the initializer list as a template argument (using a type deduction guide for example):

container(std::initializer_list<T> list) : _size(list.size()) {
    std::copy(std::begin(list), std::end(list), _arr);
}
template<typename L, 
         typename T = L::value_type, 
         typename = std::enable_if<std::is_same_v<L, std::initializer_list<T>>> >
container(L init_list) -> container<T, init_list.size()>;

This obviously does not compile (error: template argument 2 is invalid).

Is there a way realize this, preferably in a constexpr way?

1

There are 1 best solutions below

1
Artyer On

What you have is similar to inplace_vector / static_vector. These have had a lot of thought put in to them and chose to not support CTAD precisely because the maximum capacity isn't readily deducible.

However, if your use case is that the initial size is usually the maximum size, you can do it with a mix of option 2 and 3. You can't get the size of an initializer_list argument at compile time, but a deduction guide doesn't have to correspond to a constructor. You can do something like this:

template<typename T, std::size_t N>
class container {
    // ...
    // initializer_list constructor, like in (3)
    container(std::initializer_list<T> list) : _size(list.size()) {
        std::copy(std::begin(list), std::end(list), _arr);
    }
};

// Parameter pack deduction guide, like in (2)
template<typename... T>
container(T...) -> container<std::common_type_t<T...>, sizeof...(T)>;

container c{1, 2, 3};
// container<int, 3> deduced from deduction guide, initializer_list<int> constructor used