有一个...
° 介绍
正如@Yakk在他对我的另一个答案的评论中指出的那样(没有明确地说),不可能'计算'模板声明的参数的数量。但是,可以'计算'传递给实例化模板的参数的数量。
就像我的另一个答案所展示的那样,计算这些参数相当容易。
所以...
如果不能计算参数...
如何才能在不知道此模板应接收的参数数量的情况下实例化一个模板???
注意
如果你想知道为什么单词实例化(d)在本文中被划掉,
你会在脚注中找到它的解释。所以请继续阅读... ;)
° 搜索过程
- 如果我们可以以递增的参数数量尝试实例化模板,然后使用 SFINAE(Substitution Failure Is Not An Error)检测失败时,就可以找到一个通用解决方案来解决这个问题... 你不觉得吗?
- 显然,如果我们想要管理非类型参数,那就没戏了。
- 但是对于只有 typename 参数的模板...
还是有办法的...
以下是我们应该能够实现它所需的元素:
一个只有使用
typename
参数声明的模板类
可以接收任何类型作为参数。实际上,尽管可以为特定类型定义专门化,但是
主模板无法强制其参数的类型。
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- 从C++20概念开始,上述语句可能不再正确。我现在无法尝试,但@Yakk似乎对此非常自信。在他的评论之后:
我认为概念打破了这一点。“主模板无法强制其参数的类型。”是错误的。
- 如果在模板实例化之前应用约束条件,则他可能是正确的。但是...
- 通过快速跳转到约束和概念的介绍,可以在第一个代码示例之后阅读到:
“违反约束条件会在编译时检测到,在模板实例化过程的早期,这导致易于遵循的错误消息。”
完全可以创建一个模板,其唯一目的是与任意数量的参数一起实例化。对于我们这里的用例,它可能仅包含int
...(让我们称之为IPack
)。
可以定义IPack
的成员模板来通过将int
添加到当前IPack
的参数中来定义下一个IPack
。因此,可以逐步增加其参数数量...
这里可能是缺失的部分。这是大多数人没有意识到的东西。
- (我的意思是,当模板访问其参数之一必须具有的成员或者当特征测试存在特定重载时,我们大多数人都经常使用它等等...)
但我认为有时以不同的方式查看它并说:
- 可以声明一个由其他类型组装而成的任意类型,其评估可以被编译器延迟直到实际使用它。
- 因此,将
IPack
的参数注入到另一个模板中将成为可能...
最后,应该能够使用利用decltype
和std::declval
的测试特征来检测操作是否成功。(注意:最终没有使用两者之一)
° 构建模块
步骤1: IPack
template<typename...Ts>
struct IPack {
private:
template<typename U> struct Add1 {};
template<typename...Us> struct Add1<IPack<Us...>> { using Type = IPack<Us..., int>; };
public:
using Next = typename Add1<IPack<Ts...>>::Type;
static constexpr std::size_t Size = sizeof...(Ts);
};
using IPack0 = IPack<>;
using IPack1 = typename IPack0::Next;
using IPack2 = typename IPack1::Next;
using IPack3 = typename IPack2::Next;
constexpr std::size_t tp3Size = IPack3::Size; // 3
现在,一个人有了一种增加参数数量的方法,通过一个方便的方式来获取 IPack
的大小。
接下来,需要一些东西来构建任意类型,通过将 IPack
的参数注入到另一个模板中。
步骤 2: IPackInjector
一个示例,展示了如何将模板的参数注入到另一个模板中。
它使用模板特化来提取 IPack
的参数,并将其注入到 Target
中。
template<typename P, template <typename...> class Target>
struct IPackInjector ;
template<typename...Ts, template <typename...> class Target>
struct IPackInjector<IPack<Ts...>, Target> ;
template<typename T, typename U>
struct Victim;
template<typename P, template <typename...> class Target>
using IPInj = IPackInjector<P, Target>;
using V2 = typename IPInj<IPack2, Victim>::Type;
现在,有一种方法可以将IPack
的参数注入到Victim
模板中,但是,正如您所看到的,如果参数数量与Victim
模板的声明不匹配,则直接评估Type
会生成错误...
注意
您是否注意到Victim
模板没有完全定义?
它不是一个完整的类型。它只是一个模板的前向声明。
- 要测试的模板不需要是完整的类型,这个解决方案就能按预期工作... ;)
如果想要能够将这个任意构建的类型传递给某个检测特性,就必须找到一种延迟其评估的方法。
事实证明,这个“技巧”(如果可以这么说)相当简单。
这与依赖名称有关。你知道那个令人烦恼的规则,它强制你每次访问模板的成员模板时都要添加::template
。实际上,这个规则还强制编译器不评估包含依赖名称的表达式,直到它被有效使用...
- 哦,我明白了!...
所以,我们只需要准备好IPackInjector
而不访问其Type
成员,然后将其传递给我们的测试特性,对吧?可以使用类似以下的方式完成:
using TPI1 = IPackInjector<IPack1, Victim>
using TPI2 = IPackInjector<IPack2, Victim>
using TPI3 = IPackInjector<IPack3, Victim>
实际上,上面的例子没有产生错误,并且确认了有一种方法可以准备要构建的类型并在以后评估它们。
不幸的是,我们无法将这些预配置的类型构建器传递给我们的测试特性,因为我们想使用SFINAE来检测任意类型是否可以被实例化。
而这又与依赖名称有关...
SFINAE规则可以被利用来使编译器静默地选择另一个模板(或重载),只有当模板中参数的替换是依赖名称时才会发生。
简而言之:仅适用于当前模板实例的参数。
因此,为了使检测正常工作而不产生错误,用于测试的任意类型将必须在测试特性内部构建,其中至少有一个参数。测试的结果将被分配给Success
成员...
第三步:TypeTestor
template<typename T, template <typename...> class C>
struct TypeTestor {};
template<typename...Ts, template <typename...> class C>
struct TypeTestor<IPack<Ts...>, C>
{
private:
template<template <typename...> class D, typename V = D<Ts...>>
static constexpr bool Test(int) { return true; }
template<template <typename...> class D>
static constexpr bool Test(...) { return false; }
public:
static constexpr bool Success = Test<C>(42);
};
现在,最后,我们需要一台机器来连续尝试使用越来越多的参数实例化我们的Victim
模板。有几件事需要注意:
- 不能声明没有参数的模板,但可以:
- 如果测试过程以失败开始,则意味着模板必须采用更多参数。因此,测试必须继续直到成功,然后继续直到第一个失败。
- 我最初认为使用模板特化的迭代算法可能会有点复杂...但是经过一番思考,发现起始条件并不重要。
- 只需要检测上一个测试是否为
true
,下一个测试是否为false
。
- 测试次数必须有限制。
- 确实,模板可以有一个参数包,这样的模板可以接收不确定数量的参数...
步骤4:TemplateArity
template<template <typename...> class C, std::size_t Limit = 32>
struct TemplateArity
;
template<typename P, bool Last, bool Next>
struct CheckNext<0, P, Last, Next> ;
template<std::size_t I, typename P>
struct CheckNext<I, P, true, false> ;
public:
static constexpr std::size_t Max = Limit;
static constexpr std::size_t Value = CheckNext<Max, IPack<>, false, false>::Count;
};
template<typename T = int, typename U = short, typename V = long>
struct Defaulted;
template<typename T, typename...Ts>
struct ParamPack;
constexpr std::size_t size1 = TemplateArity<Victim>::Value;
constexpr std::size_t size2 = TemplateArity<Defaulted>::Value;
constexpr std::size_t size3 = TemplateArity<ParamPack>::Value;
结论
最后,解决这个问题的算法并不是那么复杂...
在找到可以解决它的“工具”之后,只是一个常常需要将正确的部分放在正确的位置上的问题... :P
尽情享受吧!
° 重要注释
这就是为什么单词 实例化(instantiated) 在与 Victim
模板相关的地方被删除的原因。
单词 实例化(instantiated) 简单来说不是正确的词...
最好使用尝试去声明,或者别名一个未来实例化的Victim
模板类型。
(这将会非常无聊):P
事实上,在这个解决方案的代码中,没有一个Victim
模板被实例化...
作为证明,足以看到在上面的代码中,所有的测试都只是在模板的前向声明上进行的。
如果你还有疑问...
using A = Victim<int>;
using B = Victim<int, int>;
template struct Victim<int, int>;
最后,介绍中可能会有一句话被删除,因为这个解决方案似乎证明了以下内容:
- 可以“计算”模板声明的参数数量...
- 无需实例化此模板。