The main issue here is that an initializer list cannot deduce a type when its contents are heterogeneous, so {1, 2, 3.F}
won't work. A secondary issue is that, even if the type is specified, you will get compiler errors if there is an implicit narrowing conversion (e.g., double
-> int
)
We have to somehow turn our heterogeneous initializer list into a homogenous collection.
Approach #1
Make a little helper function to convert your arguments into an appropriate std::array<T, M>
:
template<class T, class... Ts>
auto make_array(Ts&&... args) -> std::array<T, sizeof...(Ts)>
{
return {static_cast<T>(std::forward<Ts>(args))...};
}
It's not so pretty at the callsite:
Matrix<2,3,int> m{ make_array<int>(1,2.0,3.F), make_array<int>(4.0,5U,6)};
But it's fairly straightforward to write your constructor:
template<class... Ts, std::enable_if_t<sizeof...(Ts) == N && (std::is_same_v<Ts, std::array<T, M>> && ...), int> = 0>
Matrix(Ts&&... args) : data{std::forward<Ts>(args)...}
{
}
Approach #2
We do a lot of work to make it slightly prettier from the callsite
Here's an idea:
Allow for each member of the list to be a variant<T...>
for each possible type in the list, then accept a number of these lists of variants into your matrix for construction.
So basically, convert {1, 2, 3.0}
into a std::array<std::variant<int, double>, 3>
And then take a variadic number of these arrays as arguments to a constructor for your matrix, with appropriate type-constraining on them (each array is the appropriate size, each type in the variant is convertible to T
).
Inside your constructor, you can use a visitor to perform conversion of each array value to T
. Since std::variant
is C++17 and you're stuck with C++14, you can use a boost::variant
(and appropriate visitor).
Unfortunately what follows is quite a lot of boilerplate. Probably someone else can find something simpler
I will use a few C++17 features (a fold expression, std::disjunction
), but these can be implemented in C++14 (I do use a technique called simple template expansion that is C++14 compatible, in place of a fold expression).
(Or skip straight to the Demo)
First things first, since a std::variant
allows the same type to appear in its type list multiple times (e.g., std::variant<int, double, int>
), we should prefer to unique-ify this type list to avoid the ambiguities that come with it (this part is not exactly necessary, but I encourage it).
To do so, we are going to need a few helper types for our meta-programming
- a
typelist
to represent an arbitrary collection of types
- a
unique_typelist
that will transform a typelist
into a typelist
of unique types
variant_from_typelist
to convert a typelist
into a std::variant<T...>
And finally a way to create a std::array<std::variant<T...>, N>
given some set of arguments of type T...
.
First I'll show you the types, and then do my best to explain them:
1. a typelist
to represent an arbitrary collection of types:
template<class...>
struct typelist{};
This is our easiest type to understand. We could use tuple<T...>
, but this is lighter-weight since we aren't actually carrying around any values.
2. a unique_typelist
that will transfrom a typelist
into a typelist
of unique types
This here is the tricky bit that requires its own extra boilerplate:
- a way to detect if a type
Head
appears in a variadic typelist Tail...
For that, we'll use std::disjunction
in conjunction with std::same_as
to do this detection (note: you may want to remove cvrefs in a fully-functioning impl):
template<class Head, class... Tail>
using is_present = std::disjunction<std::is_same<Head, Tail>...>;
is_present
becomes std::true_type
if Head
is the same as any type in the variadic Tail
list.
Then we need a way to incrementally build our unique typelist for each type in a pack
- If
Head
does appear in our pack, the resulting typelist
should not include Head
- Else, since
Head
doesn't appear in our pack, we want to construct a typelist of concatenate(typelist<Head>, RecurseOn<Tail...>)
- where
RecurseOn<Tail...>
is pseudocode for "create a unique typelist of the members of the tail
- and
concatenate
is a meta-function for appending one typelist to another
- e.g.,
concatenate(typelist<int>, typelist<double>)
will give typelist<int, double>
So to do so, we'll enable the ability to concatenate (and in my impl I need to worry about an empty typelist
in the right-hand argument, so there is a specialization for it):
template<class... T>
struct concat;
template<class... T, class... U>
struct concat<typelist<T...>, typelist<U...>>
{
using type = typelist<T..., U...>;
};
template<class... T>
struct concat<typelist<T...>, typelist<>>
{
using type = typelist<T...>;
};
Now, with the aide of std::conditional
we can construct our set of unique types:
template<class... T>
struct unique_typelist
{
using type = typelist<>;
};
template<class Head, class... Tail>
struct unique_typelist<Head, Tail...>
{
using type = std::conditional_t<is_present<Head, Tail...>::value,
typename unique_typelist<Tail...>::type, // if condition is true
typename concat<typelist<Head>, typename unique_typelist<Tail...>::type>::type>;
};
It's not easy to wrap your head around all the recursion here, so take your time, and feel free to comment with questions for clarification.
3. convert a typelist into a std::variant<T...>
Now that we have a typelist<Ts...>
where each T
in Ts...
is unique, we write some helper classes to convert a NonUnique...
into a std::variant<Unique...>
(NonUnique
and Unique
are intended to be names for variadic template parameters).
template<class...>
struct typelist_to_variant;
template<class... T>
struct typelist_to_variant<typelist<T...>>
{
using type = std::variant<T...>;
};
template<class...>
struct unique_typelist_to_variant;
template<class... T>
struct unique_typelist_to_variant<unique_typelist<T...>>
{
using type = typename typelist_to_variant<typename unique_typelist<T...>::type>::type;
};
template<class... T>
struct variant_from_types
{
using type = typename unique_typelist_to_variant<unique_typelist<T...>>::type;
};
template <class... T>
using variant_from_types_t = typename variant_from_types<T...>::type;
This is better read from the bottom-up:
Given some set of possibly-duplicate types T...
, we:
- convert
T...
into a typelist<U...>
where each U
is unique, then
- strip the
typelist
part away from U...
to be placed into a std::variant
4. create a std::arraystd::variant<T..., N> given some set of arguments of type T...
Given what I just put you through, this is fairly straightforward:
template<class... T>
constexpr auto make_variant_array(T&&... args) -> std::array<variant_from_types_t<T...>, sizeof...(T)>
{
return {std::forward<T>(args)...};
};
We already wrote the variant_from_types_t
helper to get our std::variant<U...>
so that all types in U...
are unique, now it's just a matter of passing along some arguments to construct our array.
5. Create a constrained-constructor for Matrix
that accepts a variadic number of these "arrays of variant" and then initializes its data with a visitor.
To constrain the template, we're interested in enforcing that each array is of size M
, and that there are N
such arrays. For that, a small type-trait helper is nice:
template<size_t N, class T>
struct is_variant_array : std::false_type{};
template<size_t N, class... T>
struct is_variant_array<N, std::array<std::variant<T...>, N>> : std::true_type{};
is_variant_array
asks the question "Is T
an array of size N
where each element is a std::variant
?"
Let's see it in action in our Matrix
constructor:
template <std::size_t N, std::size_t M, typename T>
class Matrix{
public:
template<class... Ts, std::enable_if_t<sizeof...(Ts) == N && (is_variant_array<M, Ts>::value && ...), int> = 0>
Matrix(Ts&&... args)
{ /*...*/}
(I leave it as an exercise to the reader to further constrain these arrays such that each type in their variants is convertible to T
).
6. Use a visitor to convert each variant into T
to initialize our data
member:
For this, we'll write a fairly straightforward visitor that attempts to convert everything it gets to some type T
via static_cast
:
template<class T>
struct cast_visitor
{
template<class U>
T operator()(U u) const
{
return static_cast<T>(u);
}
};
Then, we'll use it inside our constructor with a little pre-fold expreesion trick I (we?) call simple expansion that involves a discarded-value expression and a comma operator to enforce ordering:
cast_visitor<T> visitor;
std::size_t i = 0;
auto add_row = [&, this](auto varray)
{
for(size_t j = 0; j < M; ++j)
{
data[i][j] = std::visit(visitor, varray[j]);
}
};
using swallow = size_t[];
(void)swallow{(void(add_row(args)), ++i)...};
7. Finally we are ready to call our constructor!
Matrix<2,3,int> m{ make_variant_array(1,2,3), make_variant_array(4,5,6)};
It's not pretty, but it gets the job done.