2

I need a constexpr wrapper over a C array or a std::array with some extra constructors (similar to std::vector constructors):

template<class T, int N>
struct wrapper {
  T data[N];

  constexpr wrapper(int s);         // a
  constexpr wrapper(int j, int k);  // b

  constexpr wrapper(...values...)   // c
  : data(...values...) {} 
};

I am trying to get the same behavior as the std::vector constructors, that is:

constexpr wrapper<T,2> w(1);   // calls a
constexpr wrapper<T,2> w(1,2); // calls b
constexpr wrapper<T,2> w{1};   // calls c
constexpr wrapper<T,2> w{1,2}; // calls c

Note 1: a perfect forwarding constructor:

template<class... Args>
constexpr wrapper(T&& t, Args&&... args) 
: data(std::forward<T>(t), std::forward<Args>(args)...) {}

would win over the other constructors.

Note 2: T[N]/std::array<T,N> have no std::initializer_list<T> constructors so the following doesn't work either:

constexpr wrapper(std::initializer_list<T> v) : data(std::move(v)) {}

Note 3: if the values are not assigned in the constructor initialization list the wrapper type won't work in constant expressions.

T.C.
  • 133,968
  • 17
  • 288
  • 421
gnzlbg
  • 7,135
  • 5
  • 53
  • 106

1 Answers1

3

Sure, just use index_sequence and a delegating constructor:

  constexpr wrapper(std::initializer_list<T> v)
    : wrapper(std::make_index_sequence<N>{}, v) {}
private:
  template<std::size_t... Is> constexpr wrapper(
      std::index_sequence<Is...>, std::initializer_list<T> v)
    : data{v.begin()[Is]...} {}

This works because per [support.initlist.access] the accessors of initializer_list are constexpr; its iterator type is E const* so we can access any defined member of its array at compile time.

Note that if you supply a too-short braced-init-list (such that v.size() < N) then this will fail; at compile time if used in a constant-expression, and with undefined behavior otherwise. Here's a solution, default-constructing T if allowed, and throwing an exception otherwise:

  constexpr static T too_short(std::true_type) { return {}; }
  T too_short(std::false_type) {
    throw std::invalid_argument("braced-init-list too short"); }
  template<std::size_t... Is> constexpr wrapper(
      std::index_sequence<Is...>, std::initializer_list<T> v)
    : data{v.size() > Is ? v.begin()[Is]
        : too_short(std::is_default_constructible<T>{})...} {}

If you want the initialization to always fail when called with a too-short braced-init-list, just replace the right-hand branch of the conditional operator with a throw-expression:

  constexpr wrapper(std::initializer_list<T> v)
    : wrapper(std::make_index_sequence<N>{}, v.size() == N ? v
        : throw std::invalid_argument("braced-init-list incorrect length")) {}
private:
  template<std::size_t... Is> constexpr wrapper(
      std::index_sequence<Is...>, std::initializer_list<T> v)
    : data{v.begin()[Is]...} {}

It is not possible other than in a constant-expression portably to detect at compile time that the braced-init-list is too short (or too long!), because initializer_list hides that information from the type system.

Full example.

ecatmur
  • 152,476
  • 27
  • 293
  • 366