具有不同类型算术的嵌套可变参数模板参数的构造函数 Demo 1fold expreesion

问题描述

跟进initialize double nested std::array from variadic template array reference constructor

问题

我有一个 Matrix 班,可以做一些数学运算。但这纯粹是关于初始化...

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

可以按如下方式初始化它(效果很好)

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

但我想允许可变参数模板构造函数接受初始化列表中的不同类型。在这种情况下,它应该应用缩小。 例如:

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

我完全知道这在这里行不通,因为构造函数只接受与使用 Matrix 定义的 std::is_same 相同的类型。我将构造函数更改为使用 std::is_arithmetic(因为该类应该只接受算术类型或此类的引用。它还包含一个静态断言,用于在初始化时检查模板参数 T,但这并不重要在这里

新构造函数

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

clang 错误

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

据我所知,不能对初始化列表、初始化列表内的值进行推导。

因为这是一个双重嵌套,所以我有这个问题。如果可变参数模板构造函数不是嵌套的,这可以正常工作。它只需要一个 static_cast 或对编译器选项的更改,以使编译器对隐式缩小保持沉。例如,我有一个 Vector 可以按照我想要的方式工作。

Vector

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

示例代码

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

Demo

问题

是否有可能使 Matrix 中的嵌套可变参数模板构造函数接受各个值的不同类型,以便在需要时应用缩小以匹配模板类型 T

解决方法

这里的主要问题是初始化列表在其内容异构时无法推导出类型,因此 {1,2,3.F} 将不起作用。第二个问题是,即使指定了类型,如果存在隐式收缩转换(例如,double -> int

,您也会收到编译器错误

我们必须以某种方式将我们的异构初始化列表变成一个同构集合。

方法#1

制作一个小辅助函数来将您的参数转换为合适的 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))...};
}

在呼叫站点上不是那么漂亮:

Matrix<2,3,int> m{ make_array<int>(1,2.0,3.F),make_array<int>(4.0,5U,6)};

但是编写构造函数相当简单:

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

方法#2

我们做了大量工作,使其从呼叫站点变得更漂亮

这是一个想法:

允许列表中的每个成员都是列表中每种可能类型的 variant<T...>,然后将这些变体列表中的一些接受到您的矩阵中以进行构建。

所以基本上,将 {1,3.0} 转换为 std::array<std::variant<int,double>,3>

然后将这些数组的可变数作为矩阵构造函数的参数,对它们进行适当的类型约束(每个数组都有适当的大小,变体中的每种类型都可以转换为 T) .

在您的构造函数中,您可以使用访问者将每个数组值转换为 T。由于 std::variant 是 C++17 并且您坚持使用 C++14,因此您可以使用 boost::variant(和适当的访问者)。

不幸的是接下来是大量的样板文件。也许其他人可以找到更简单的东西

我将使用一些 C++17 特性(折叠表达式,std::disjunction),但这些可以在 C++14 中实现(我确实使用一种称为简单的技术与 C++14 兼容的模板扩展,代替折叠表达式)。

(或直接跳到Demo


首先,由于 std::variant 允许同一类型多次出现在其类型列表中(例如,std::variant<int,double,int>),我们应该更倾向于唯一化此类型列表以避免歧义随之而来的(这部分不是完全必要的,但我鼓励它)。

为此,我们需要一些用于元编程的辅助类型

  1. 一个 typelist 表示任意类型的集合
  2. unique_typelist 转换为唯一类型的 typelisttypelist
  3. variant_from_typelisttypelist 转换为 std::variant<T...>

最后一种在给定一些类型为 std::array<std::variant<T...>,N> 的参数集的情况下创建 T... 的方法。

首先我将向您展示类型,然后尽力解释它们:

1.表示任意类型集合的 typelist

template<class...>
struct typelist{};

这是我们最容易理解的类型。我们可以使用 tuple<T...>,但这更轻,因为我们实际上没有携带任何值。

2.一个 unique_typelist 将从一个 typelist 转换为一个唯一类型的 typelist

这是需要自己额外样板的棘手部分:

  • 一种检测类型 Head 是否出现在可变参数类型列表 Tail... 中的方法

为此,我们将结合使用 std::disjunctionstd::same_as 来执行此检测(注意:您可能希望删除功能齐全的 impl 中的 cvref):

template<class Head,class... Tail>
using is_present = std::disjunction<std::is_same<Head,Tail>...>;
如果 is_present 与可变参数 std::true_type 列表中的任何类型相同,

Head 变为 Tail

然后我们需要一种方法来为包中的每个类型逐步构建我们唯一的类型列表

  1. 如果 Head 确实出现在我们的包中,则生成的 typelist 不应包含 Head
    • 仅在 Tail... 上递归
  2. 否则,由于 Head 没有出现在我们的包中,我们想要构造一个 concatenate(typelist<Head>,RecurseOn<Tail...>) 的类型列表
    • 其中 RecurseOn<Tail...> 是“创建尾部成员的唯一类型列表”的伪代码
    • concatenate 是一个元函数,用于将一个类型列表附加到另一个类型列表
      • 例如,concatenate(typelist<int>,typelist<double>) 会给 typelist<int,double>

为此,我们将启用连接的能力(在我的实现中,我需要担心右侧参数中的空 typelist,因此有一个专门化):

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

现在,在 std::conditional 的帮助下,我们可以构建独特的类型集:

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

要了解这里的所有递归并不容易,所以请花点时间,并随时发表评论以澄清问题。

3.将类型列表转换为 std::variant

既然我们有一个 typelist<Ts...>,其中 T 中的每个 Ts... 都是唯一的,我们编写一些帮助类来将 NonUnique... 转换为 std::variant<Unique...> ( NonUniqueUnique 旨在作为可变参数模板参数的名称)。

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; 

最好从下往上阅读: 给定一些可能重复的类型 T...,我们:

  • T... 转换为 typelist<U...>,其中每个 U 都是唯一的,然后
  • typelist 部分从 U... 剥离以放入 std::variant

4.给定一些 T 类型的参数集,创建一个 std::arraystd::variant

根据我刚刚介绍的内容,这相当简单:

template<class... T>
constexpr auto make_variant_array(T&&... args) -> std::array<variant_from_types_t<T...>,sizeof...(T)>
{
    return {std::forward<T>(args)...};
};

我们已经编写了 variant_from_types_t 助手来获取我们的 std::variant<U...>,因此 U... 中的所有类型都是唯一的,现在只需传递一些参数来构造我们的数组。

5.为 Matrix 创建一个受约束的构造函数,它接受这些“变体数组”的可变参数,然后使用访问者初始化其数据。

为了约束模板,我们有兴趣强制每个数组的大小为 M,并且有 N 个这样的数组。为此,一个小的类型特征助手很好:

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 提出问题“T 是一个大小为 N 的数组,其中每个元素都是一个 std::variant?”

让我们看看它在我们的 Matrix 构造函数中的作用:

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

(我将其作为练习留给读者,以进一步约束这些数组,使其变体中的每种类型都可以转换为 T)。

6.使用访问者将每个变体转换为 T 以初始化我们的 data 成员:

为此,我们将编写一个相当简单的访问者,尝试通过 T 将其获取的所有内容转换为某种类型 static_cast

template<class T>
struct cast_visitor
{
    template<class U>
    T operator()(U u) const
    {
        return static_cast<T>(u);
    }
};

然后,我们将在我们的构造函数中使用它在我们的构造函数中使用一个小技巧,我(我们?)调用简单的扩展,它涉及一个丢弃值表达式和一个逗号运算符来强制排序:

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.最后,我们准备好调用我们的构造函数了!

Matrix<2,int> m{ make_variant_array(1,3),make_variant_array(4,5,6)};

它不漂亮,但它完成了工作。

fold expreesion