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

mto*_*sch 1 c++ constructor sfinae variadic-templates c++14

跟进从可变参数模板数组引用构造函数初始化双重嵌套的 std::array

问题

我有一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
};
Run Code Online (Sandbox Code Playgroud)

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

Matrix<2, 2, int> m{ {3,4}, {5,6} };
Run Code Online (Sandbox Code Playgroud)

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

Matrix<2, 2, int> m{ {3,4}, {5,6.5F} }; // does not compile
// should be [ [3,4], [5,6] ]
Run Code Online (Sandbox Code Playgroud)

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

新构造函数

Matrix<2, 2, int> m{ {3,4}, {5,6} };
Run Code Online (Sandbox Code Playgroud)

铿锵错误

<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)...} {}
Run Code Online (Sandbox Code Playgroud)

据我了解,不能对初始值设定项列表内的值进行推导。

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

Vector

Matrix<2, 2, int> m{ {3,4}, {5,6.5F} }; // does not compile
// should be [ [3,4], [5,6] ]
Run Code Online (Sandbox Code Playgroud)

示例代码

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)...} {}
Run Code Online (Sandbox Code Playgroud)

演示

问题

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

And*_*dyG 5

这里的主要问题是,当初始化列表的内容是异构的时,它无法推断出类型,因此{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))...};
}
Run Code Online (Sandbox Code Playgroud)

调用站点的情况并不那么漂亮:

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

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

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)...}
{
}
Run Code Online (Sandbox Code Playgroud)

演示1

方法#2

我们做了很多工作来使其从调用站点变得更漂亮

这是一个想法:

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

所以基本上,转换{1, 2, 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 兼容,代替折叠表达)。

(或者直接跳到演示


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

为此,我们需要一些辅助类型来进行元编程

  1. atypelist表示任意类型的集合
  2. aunique_typelist会将 a 转换typelisttypelist独特类型的a
  3. variant_from_typelist将 a 转换typelist为 astd::variant<T...>

最后是创建一std::array<std::variant<T...>, N>组给定的类型参数的方法T...

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

1. atypelist表示任意类型的集合:

template<class...>
struct typelist{};
Run Code Online (Sandbox Code Playgroud)

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

2. aunique_typelist将从 a 转换typelisttypelist独特类型的a

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

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

为此,我们将std::disjunction结合使用std::same_as来进行此检测(注意:您可能希望在功能齐全的实现中删除 cvrefs):

template<class Head, class... Tail>
using is_present = std::disjunction<std::is_same<Head, Tail>...>;
Run Code Online (Sandbox Code Playgroud)

is_presentstd::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...>;
};
Run Code Online (Sandbox Code Playgroud)

现在,在 的帮助下,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>;
};
Run Code Online (Sandbox Code Playgroud)

理解这里的所有递归并不容易,所以请慢慢来,并随时评论问题以进行澄清。

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

现在我们有了 a ,typelist<Ts...>其中每个TinTs...都是唯一的,我们编写一些辅助类来将 a 转换NonUnique...为 a std::variant<Unique...>NonUnique并且Unique旨在作为可变参数模板参数的名称)。

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; 
Run Code Online (Sandbox Code Playgroud)

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

  • 转换T...typelist<U...>每个U都是唯一的,然后
  • typelist零件从其上U...剥离并放入std::variant

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

鉴于我刚刚向您介绍的内容,这相当简单:

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

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

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

为了约束模板,我们有兴趣强制每个数组的 size 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{};
Run Code Online (Sandbox Code Playgroud)

is_variant_array提出问题“是否是一个大小为每个元素都是 aT的数组?”Nstd::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)
{ /*...*/}
Run Code Online (Sandbox Code Playgroud)

(我将其作为练习留给读者,以进一步约束这些数组,以便其变体中的每种类型都可以转换为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);
    }
};
Run Code Online (Sandbox Code Playgroud)

然后,我们将在构造函数中使用它,并使用一些预折叠表达式技巧,我(我们?)称之为简单扩展,其中涉及丢弃值表达式和逗号运算符来强制排序:

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)...};
Run Code Online (Sandbox Code Playgroud)

7. 最后我们准备好调用我们的构造函数了!

Matrix<2,3,int> m{ make_variant_array(1,2,3), make_variant_array(4,5,6)};
Run Code Online (Sandbox Code Playgroud)

它并不漂亮,但它可以完成工作。

演示2