Home > database >  Compile-time concatenation of std::initializer_list's
Compile-time concatenation of std::initializer_list's

Time:11-16

I would like to write some code like the following:

using int_list_t = std::initializer_list<int>;

struct ThreeDimensionalBox {
  static constexpr int_list_t kDims = {1, 2, 3};
};

struct FourDimensionalBox {
  static constexpr int_list_t kDims = {4, 5, 6, 7};
};

template<typename Box1, typename Box2>
struct CombinedBox {
  static constexpr int_list_t kDims = Box1::kDims   Box2::kDims;  // error
};

using SevenDimensionalBox = CombinedBox<ThreeDimensionalBox, FourDimensionalBox>;

Is there some way to fix the implementation of CombinedBox, so that SevenDimensionalBox::kDims is effectively bound to {1, 2, 3, 4, 5, 6, 7}?

I know that I can replace std::initializer_list<int> with a custom template class with a variadic int template parameter list, with concatenation effectively achieved via standard metaprogramming recursion techniques. I was just wondering if a solution exists using only std::initializer_list.

CodePudding user response:

std::initializer_list can be initialized only as empty, with a list of brace-enclosed elements, or by copy.

However even with a copy construction, the lifetime of the actual array that std::initializer_list references is determined by the lifetime of the original std::initializer_list object that was initialized by a brace-enclosed element list. In other words copying the std::initializer_list does not copy the array or extend its lifetime.

Therefore it is impossible to concatenate std::initializer_lists. They are not meant to be used as a container. A std::initializer_list should mostly be used only as a function parameter as a lightweight way to pass a brace-enclosed list of elements of unspecified size to a function for it to then process the elements further.

You probably want std::array instead. Something like this (requiring C 17, but not C 20):

struct ThreeDimensionalBox {
  static constexpr auto kDims = std::array{1, 2, 3};
};

struct FourDimensionalBox {
  static constexpr auto kDims = std::array{4, 5, 6, 7};
};

template<typename T, std::size_t N, std::size_t M>
constexpr auto concat_arrays(const std::array<T, N>& a, const std::array<T, M>& b) {
    // assumes `T` is default-constructible
    std::array<T, N M> r;
    std::copy(std::begin(a), std::end(a), std::begin(r));
    std::copy(std::begin(b), std::end(b), std::begin(r) N);
    return r;
}

template<typename Box1, typename Box2>
struct CombinedBox {
  static constexpr auto kDims = concat_arrays(Box1::kDims, Box2::kDims);
};

using SevenDimensionalBox = CombinedBox<ThreeDimensionalBox, FourDimensionalBox>;

CodePudding user response:

std::initializer_list has exactly and only one purpose: to be a tool for initializing an object, within the scope where a braced-init-list (the stuff in {}) was used for the initialization of that object. Everything about this type is built for that purpose, and it has a bunch of firewalls in place to prevent you from doing basically anything else with it.

The most important of which is that initializer_list doesn't actually store anything; it references an array of values which is stored elsewhere (namely, the cite of the {} that created it). Since it does not store the array, and they're only meant to be build from the {} grammatical construct, you cannot give them an array to reference. They can only get an array from an existing initializer_list object via copying, and that one could only get one either from a {} or an existing initializer_list which could only get one from... well, you get the idea.

If you need to do compile-time array manipulation, std::array is a better tool. If you want to concatenate two braced-init-lists and use the result to initialize some object... you can't do that.

Do not try to use initializer_list as a quick-and-dirty array type. Only use it for initializing objects.

  • Related