Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing std::array-like constructors in other classes

In all the modern C++ compilers I've worked with, the following is legal:

std::array<float, 4> a = {1, 2, 3, 4};

I'm trying to make my own class that has similar construction semantics, but I'm running into an annoying problem. Consider the following attempt:

#include <array>
#include <cstddef>

template<std::size_t n>
class float_vec
{
private:
  std::array<float, n> underlying_array;

public:
  template<typename... Types>
  float_vec(Types... args)
    : underlying_array{{args...}}
  {
  }
};

int main()
{
  float_vec<4> v = {1, 2, 3, 4}; // error here
}

When using int literals like above, the compiler complains it can't implicitly convert int to float. I think it works in the std::array example, though, because the values given are compile-time constants known to be within the domain of float. Here, on the other hand, the variadic template uses int for the parameter types and the conversion happens within the constructor's initializer list where the values aren't known at compile-time.

I don't want to do an explicit cast in the constructor since that would then allow for all numeric values even if they can't be represented by float.

The only way I can think of to get what I want is to somehow have a variable number of parameters, but of a specific type (in this case, I'd want float). I'm aware of std::initializer_list, but I'd like to be able to enforce the number of parameters at compile time as well.

Any ideas? Is what I want even possible with C++11? Anything new proposed for C++14 that will solve this?

like image 400
jbatez Avatar asked Jan 24 '14 21:01

jbatez


3 Answers

A little trick is to use constructor inheritance. Just make your class derive from another class which has a pack of the parameters you want.

template <class T, std::size_t N, class Seq = repeat_types<N, T>>
struct _array_impl;

template <class T, std::size_t N, class... Seq>
struct _array_impl<T, N, type_sequence<Seq...>>
{
    _array_impl(Seq... elements) : _data{elements...} {}
    const T& operator[](std::size_t i) const { return _data[i]; }

    T _data[N];
};


template <class T, std::size_t N>
struct array : _array_impl<T, N>
{
    using _array_impl<T, N>::_array_impl;
};

int main() {
    array<float, 4> a {1, 2, 3, 4};
    for (int i = 0; i < 4; i++)
        std::cout << a[i] << std::endl;
    return 0;
}

Here is a sample implementation of the repeat_types utility. This sample uses logarithmic template recursion, which is a little less intuitive to implement than with linear recursion.

template <class... T>
struct type_sequence
{
    static constexpr inline std::size_t size() noexcept { return sizeof...(T); }
};


template <class, class>
struct _concatenate_sequences_impl;
template <class... T, class... U>
struct _concatenate_sequences_impl<type_sequence<T...>, type_sequence<U...>>
    { using type = type_sequence<T..., U...>; };
template <class T, class U>
using concatenate_sequences = typename _concatenate_sequences_impl<T, U>::type;


template <std::size_t N, class T>
struct _repeat_sequence_impl
    { using type = concatenate_sequences<
        typename _repeat_sequence_impl<N/2, T>::type,
        typename _repeat_sequence_impl<N - N/2, T>::type>; };
template <class T>
struct _repeat_sequence_impl<1, T>
    { using type = T; };
template <class... T>
struct _repeat_sequence_impl<0, type_sequence<T...>>
    { using type = type_sequence<>; };
template <std::size_t N, class... T>
using repeat_types = typename _repeat_sequence_impl<N, type_sequence<T...>>::type;
like image 139
Alex B Avatar answered Sep 30 '22 04:09

Alex B


First of what you are seeing is the default aggregate initialization. It has been around since the earliest K&R C. If your type is an aggregate, it supports aggregate initialization already. Also, your example will most likely compile, but the correct way to initialize it is std::array<int, 3> x ={{1, 2, 3}}; (note the double braces).

What has been added in C++11 is the initializer_list construct which requires a bit of compiler magic to be implemented.

So, what you can do now is add constructors and assignment operators that accept a value of std::initializer_list and this will offer the same syntax for your type.

Example:

#include <initializer_list>

struct X {
  X(std::initializer_list<int>) {
    // do stuff
  }
};

int main()
{
  X x = {1, 2, 3};

  return 0;
}

Why does your current approach not work? Because in C++11 std::initializer_list::size is not a constexpr or part of the initializer_list type. You cannot use it as a template parameter.

A few possible hacks: make your type an aggregate.

#include <array>

template<std::size_t N>
struct X {
  std::array<int, N> x;
};

int main()
{
  X<3> x = {{{1, 2, 3}}}; // triple braces required
  return 0;
}

Provide a make_* function to deduce the number of arguments:

#include <array>

template<std::size_t N>
struct X {
  std::array<int, N> x;
};

template<typename... T>
auto make_X(T... args) -> X<sizeof...(T)>
// might want to find the common_type of the argument pack as well
{ return X<sizeof...(T)>{{{args...}}}; } 

int main()
{
  auto x = make_X(1, 2, 3);
  return 0;
}
like image 35
pmr Avatar answered Sep 30 '22 03:09

pmr


If you use several braces to initialize the instance, you can leverage list-init of another type to accept these conversions for compile-time constants. Here's a version that uses a raw array, so you only need parens + braces for construction:

#include <array>
#include <cstddef>

template<int... Is> struct seq {};
template<int N, int... Is> struct gen_seq : gen_seq<N-1, N-1, Is...> {};
template<int... Is> struct gen_seq<0, Is...> : seq<Is...> {};

template<std::size_t n>
class float_vec
{
private:
  std::array<float, n> underlying_array;

  template<int... Is>
  constexpr float_vec(float const(&arr)[n], seq<Is...>)
    : underlying_array{{arr[Is]...}}
  {}

public:
  constexpr float_vec(float const(&arr)[n])
    : float_vec(arr, gen_seq<n>{})
  {}
};

int main()
{
  float_vec<4> v0 ({1, 2, 3, 4});   // fine
  float_vec<4> v1 {{1, 2, 3, 4}};   // fine
  float_vec<4> v2 = {{1, 2, 3, 4}}; // fine
}
like image 24
dyp Avatar answered Sep 30 '22 03:09

dyp