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.

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.