jbatez
jbatez

Reputation: 1812

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?

Upvotes: 10

Views: 1342

Answers (4)

Alex B
Alex B

Reputation: 66

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;

Upvotes: 5

pmr
pmr

Reputation: 59811

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;
}

Upvotes: 3

dyp
dyp

Reputation: 39101

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
}

Upvotes: 1

Brandon
Brandon

Reputation: 23500

Explicitly specify that the data type for the initialization to floating point type. You can do this by doing "1.0f" instead of putting "1". If it is a double, put "1.0d". If it is a long, put "1l" and for unsigned long put "1ul" and so on..

I've tested it here: http://coliru.stacked-crooked.com/a/396f5d418cbd3f14 and here: http://ideone.com/ZLiMhg

Your code was fine. You just initialized the class a bit incorrect.

#include <array>
#include <cstddef>
#include <iostream>

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...}}
  {
  }

  float get(int index) {return underlying_array[index];}
};

int main()
{
  float_vec<4> v = {1.5f, 2.0f, 3.0f, 4.5f}; //works fine now..

  for (int i = 0; i < 4; ++i)
    std::cout<<v.get(i)<<" ";
}

Upvotes: -1

Related Questions