继承与参数模板包。诀窍是什么?

Inheritance together with parameter template packs. What is the trick?

我看到一些代码 () 使用递归 "inheritance" 来破坏模板参数包。该建筑背后的想法是什么?它是如何工作的?

前言

虽然模板递归通常是必需的,但没有什么是必须使用继承的。也就是说,使用模式已经相当标准:

template <typename T, typename = void>
struct has_trait : std::false_type {};

template <typename T>
struct has_trait<T, std::enable_if_t<some_predicate>> : std::true_type {};

也就是说,为了方便起见,标准提供了 std::{true,false}_type 类型,因为它们具有静态 constexpr value 成员。这种模式强烈表明递归继承是常见的并且受到鼓励。

递归继承

假设我想确定一个参数包是否包含T类型的参数。为此,我将创建一个 struct 类型的实现:

template <bool B, typename T, typename ... ARGS>
struct pack_with_type_impl;

非类型参数B是一个"matching"参数; T 是我们有兴趣匹配的参数; ARGS... 是参数包。

我们将使用此模板的特殊化来处理各种情况。

空参数包

我们以参数包为空的情况为例。在那种情况下,专业化看起来像:

template <bool B, typename T>
struct pack_with_type_impl<B,T> : std::false_type {};

空参数包中显然没有类型T,所以特化继承自std::false_type

匹配参数包

现在假设已经确定T类型的参数存在于参数包中。在那种情况下,专业化看起来像:

template <typename T, typename ... ARGS>
struct pack_with_type_impl<true, T, ARGS...> : std::true_type {};

递归

现在是有趣的部分。到目前为止,我们还没有进行递归,因为上述情况代表了终止条件:一种是参数包为空,因此无事可做;和一个已经找到匹配项的地方,所以没有什么可做的了。但现在让我们来完成 尚未 找到匹配项的部分。在那种情况下,我们会有这样的东西:

template <typename T, typename H, typename ... TAIL>
struct pack_with_type_impl<false, T, H, TAIL...> :
   pack_with_type_impl<std::is_same<T,H>::value, T, TAIL...> {};

什么?在本例中,匹配项是 false,因此它将从模板 继承,少提供一个参数 。也就是参数包头部对应的参数H被剥离掉了,这样自己测试,保留类型TTAIL...如有必要,供将来处理。 std::is_same 功能只是检查类型是否相同。继承链继续前进,每次都剥离头部,直到达到终止条件之一。

举个例子,假设我想检查参数包的类型是否为 int,我提供的参数是:char,double,float。继承链看起来像这样:

pack_with_type_impl<false,int,char,double,float> 
  ==> pack_with_type_impl<false,int,double,float> 
    ==> pack_with_type_impl<false,int,float> 
      ==> pack_with_type_impl<false,int> # and since the parameter pack is empty now...
        ==> std::false_type

另一方面,如果我提供参数 char,int,double,继承链将是:

pack_with_type_impl<false,int,char,int,float> 
  ==> pack_with_type_impl<false,int,int,float> 
    ==> pack_with_type_impl<true,int,float> # and since the first argument is true 
      ==> std::true_type

评论

肯定有更优雅的方法可以做到这一点。对于初学者,我可能会按照以下行创建一些别名模板:

template <typename ... ARGS>
using has_int = pack_with_type_impl<false,int,ARGS...>;

这样我就可以打电话给:

template <typename ... ARGS>
void increment_int(std::tuple<ARGS...>& tup) {
   static_assert(has_int<ARGS...>::value,
                 "Can only call increment_int on tuples with an int type!");
   ...
}

但这是第一次尝试解释这个棘手的问题。

在泛型代码中,您不能创建具有 N 个直接成员的 class。但是,您可以创建具有一个直接成员和 N-1 个继承成员的 class,通过专门针对 N==1.

结束递归

示例:假设您要创建一个 class,使得 Foo<int, double, std::string> 包含 3 个成员函数 void Foo::f(int); void Foo::f(int); void Foo::f(std::string)。这不是直接可能的,因为 N=3。但是,您可以从 Foo<double, std::string> 派生 Foo<int, double, std::string> 并添加一个成员 void f(int)

template<typename HEAD, typename... Tail>
class Foo : public Foo<Tail...>
{
    public: void f(HEAD h) { std::cout << h; }
};

template<typename HEAD>
class Foo<HEAD>
{
    public: void f(HEAD h) { std::cout << h; }
};