继承与参数模板包。诀窍是什么?
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
被剥离掉了,这样自己测试,保留类型T
和TAIL...
如有必要,供将来处理。 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; }
};
我看到一些代码 () 使用递归 "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
被剥离掉了,这样自己测试,保留类型T
和TAIL...
如有必要,供将来处理。 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; }
};