1

follow up to initialize double nested std::array from variadic template array reference constructor

Problem

I have a Matrix class that does can do some math. But this is purely about initialization...

Matrix

// helper functions
template <std::size_t N, typename T, std::size_t... Is>
std::array<T, N> to_array_impl(const T (&arr)[N], std::index_sequence<Is...>) {
  return std::array<T, N>{arr[Is]...};
}

template <std::size_t N, typename T>
std::array<T, N> to_array(const T (&arr)[N]) {
  return to_array_impl(arr, std::make_index_sequence<N>{});
}

// Matrix
template <std::size_t N, std::size_t M, typename T>
class Matrix{
  public:

    template <typename... TArgs,
      std::enable_if_t<sizeof...(TArgs) == N &&
             (std::is_same_v<T, std::remove_reference_t<TArgs>> &&...), int> = 0>
    Matrix(TArgs const(&&... rows)[M]) : data_{to_array(rows)...} {}

    // ...

    private:
      std::array<Vector<M,T>, N> data; // <- custom Vector class but uses an std::array for its data
};

One can initialize it as follows (which works just fine)

Matrix<2, 2, int> m{ {3,4}, {5,6} };

But i would like to allow the variadic template constructor to accept different types inside the initializer list. In that case, it should apply narrowing. For example:

Matrix<2, 2, int> m{ {3,4}, {5,6.5F} }; // does not compile
// should be [ [3,4], [5,6] ]

I'm fully aware that this can't work here because the constructor only accepts the same type that the Matrix was defined with by using std::is_same. I changed the constructor to use std::is_arithmetic instead (because the class should only accept arithmetic types or references of such. it also contains a static asserts that checks the template parameter T at initialization, but that's not really important here)

New constructor

template <typename... TArgs,
   std::enable_if_t<sizeof...(TArgs) == N &&
          (std::is_arithmetic_v<T, std::remove_reference_t<TArgs>> &&...), int> = 0> // <--- is_arithmetic
Matrix(TArgs const(&&... rows)[M]) : data_{to_array(rows)...} {}

clang error

<source>:55:3: note: candidate template ignored: deduced conflicting types for parameter 'TArgs'  ('int' vs. 'float')
Matrix(TArgs const(&... rows)[M]) : data{to_array(rows)...} {}

As far as i understand, the deduction cannot be made for the values inside an initializer list, inside an initializer list.

Because this is a double nesting, i have this problem. If the variadic template constructor is NOT nested, this works fine. It just requires a static_cast or a change to the compiler options to silence the compiler about implicit narrowing. For example, i have a Vector that works just as i want with this.

Vector

template <typename... TArgs,
   std::enable_if_t<sizeof...(TArgs) == N &&
          (std::is_arithmetic_v<T, std::remove_reference_t<TArgs>> &&...), int> = 0>
Vector(TArgs &&... args)[M]) : data_{std::forward<TArgs>(args)...} {}
//                                   ^^ use static_cast here or general compiler option

example code

Vector<3,int> v{3, 4, 5.5F}; // compiles
// will be [3, 4, 5]

Demo

Question

Is it possible tp make the nested variadic template constructor in Matrix accept different types for the individual values, so that it applies narrowing if necesary to match the template type T?

mtosch
  • 351
  • 4
  • 18
  • 1
    *"In that case, it should apply narrowing"* list-initialization tries to _prevent_ narrowing, because that's considered error-prone... – dyp Dec 22 '20 at 14:53
  • i see. do you have a link that elaborates? – mtosch Dec 22 '20 at 14:56
  • Strangely, I cannot quickly find good examples of _why narrowing is bad_. Well, narrowing is surprising, since you loose information without any hint/warning. For example, if I write `Vector<3, int> v{1.2, 1.4, 5.4};` and I mistakenly used type `int` instead of `double`, then I'm a bit surprised that `v[0] != 1.2`. – dyp Dec 22 '20 at 16:06
  • Details about _what narrowing is_: https://en.cppreference.com/w/cpp/language/list_initialization (section "Narrowing conversions"), http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-narrowing and https://www.modernescpp.com/index.php/c-core-guidelines-rules-for-conversions-and-casts – dyp Dec 22 '20 at 16:08
  • definitely does make sense, no question. was just wondering if it's possible somehow. i guess my case is mainly "conversion from a floating-point type to an integer type" so it's not possible with list initialization. – mtosch Dec 22 '20 at 17:36
  • Is there maybe a way though to explicitly use static_cast in the Matrix constructor like i indicated in the Vector constructor? – mtosch Dec 22 '20 at 17:42
  • Re: references for narrowing conversions. See [dcl.init.aggr] that says a narrowing conversion during aggregate initialization is ill-formed. See [dcl.init.list] that says a narrowing conversion during list-initialization of a class type is ill-formed. – AndyG Dec 23 '20 at 16:06

1 Answers1

4

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

Demo 1

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

  1. a typelist to represent an arbitrary collection of types
  2. a unique_typelist that will transform a typelist into a typelist of unique types
  3. 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

  1. If Head does appear in our pack, the resulting typelist should not include Head
    • Recurse only on Tail...
  2. 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.

Demo 2

AndyG
  • 39,700
  • 8
  • 109
  • 143
  • Wow, great and detailed answer! When i experimented myself with code i didn't show here, it kind of started to have some parts of what you described here in much more detail. I feared that a caller would have to use some method like your `make_variant_array` to convert the individual types...since i want to make it as easy as possible for a caller, i won't implement this. But i can see that parts of your answer can help me with some other pieces of my internal code :) – mtosch Dec 23 '20 at 12:28
  • @mtosch: Great to hear. Fun to write some deep templating every now and then – AndyG Dec 23 '20 at 14:19