根据模板模板参数采用的参数数量部分特化模板的语法是什么?
What is the syntax for partially specialising a template based on the number of parameters a template template parameter takes?
考虑以下代码:
template<typename>
struct One {};
template<typename, typename>
struct Two {};
template<template<typename...> class TTP, typename...>
struct SS;
#ifdef TEST_TTP
template<template<typename> class OneParam,
typename... Ts>
struct SS<OneParam, Ts...> {};
template<template<typename, typename> class TwoParam,
typename... Ts>
struct SS<TwoParam, Ts...> {};
#else // TEST_TTP
template<template<typename> class OneParam,
typename TParam>
struct SS<OneParam, TParam> {};
template<template<typename, typename> class TwoParam,
typename TParam1,
typename TParam2>
struct SS<TwoParam, TParam1, TParam2> {};
#endif // TEST_TTP
int main() {
SS<One, int> ssoi;
SS<Two, int, int> sstii;
}
如果未定义 TEST_TTP
,此代码将在 Clang、GCC 和 MSVC 上正确编译。但是,如果它 是 定义的...
- 代码在 GCC 上正确编译,表明它识别出
OneParam
和 TwoParam
与主模板中的 TTP
不同。
- Clang 无法识别
OneParam
特化 TTP
,导致它发出两个错误(第一个是部分特化没有特化任何模板参数,第二个是 OneParam
与之前声明的模板模板参数冲突)。然后它为 TwoParam
发出类似的错误(第一个是相同的,而第二个说模板模板参数有太多参数),并为 SS
的每个实例化发出一个错误(因为它考虑了模板未定义),总共有 6 个错误。
- MSVC 发出与 Clang 类似的错误,但更简洁:它发出 C3855(
OneParam
与主模板不兼容),并为 [=21= 的每个实例化发出 C2079(变量使用未定义类型) ], 总共有 3 个错误。
现场演示on Coliru.
根据我的测试:
GCC 允许带有模板模板参数的模板采用可变参数包,仅根据模板模板参数采用的参数数量进行部分特化。 Clang 和 MSVC 没有。
template<template<typename...> class T> struct S;
template<template<typename> class T> struct S<T> {}; // Only works with GCC.
template<template<typename, typename> class T> struct S<T> {}; // Only works with GCC.
如果其他参数也专门化,Clang 和 MSVC 也可以。
template<template<typename...> class T, typename... Ts> struct S;
template<template<typename> class T,
typename TParam>
struct S<T, TParam> {};
template<template<typename, typename> class T,
typename TParam1,
typename TParam2>
struct S<T, TParam1, TParam2> {};
因此,前者似乎不是合法的 C++,或者它没有得到 Clang 和 MSVC 的正确支持。所以,问题是:
考虑到这一点,根据模板模板参数采用的参数数量,部分特化包含模板模板参数的模板的正确合法语法是什么?如果没有合法的语法,支持它是 GCC 扩展 and/or 错误吗?
如果需要我执行的测试的完整记录以及提示此问题的原始示例,请查看编辑历史记录。
tl;dr:你的代码是有效的,但没有编译器能正确处理它;即使是那些接受它的人(gcc 和 ICC(英特尔 C++ 编译器))也不一致或出于错误的原因这样做。
正确的推理如下:
- 可变模板模板参数可以be deduced to a single-argument template template parameter, and vice versa。 gcc 是唯一正确的编译器。
- 单参数模板是 more specialized than a variadic template。所有现代编译器都能做到这一点。
- Accordingly,采用单参数模板模板参数的函数模板比采用可变参数模板模板参数的函数模板更专业。似乎唯一正确的编译器是 ICC,但这只是因为它得到 (1) 错误。
- Accordingly,采用单参数模板模板参数的 class 模板比采用可变模板模板参数的 class 模板更专业。 ICC 和 gcc 似乎是正确的,但在前一种情况下,因为它得到 (1) 错误,并且 gcc 与 (3) 不一致。
结论:您使用的是正确的、合法的语法;但是您可能需要针对不兼容的编译器的解决方法。即使您的代码可以编译,我也会 建议使用 static_assert
来验证是否已选择预期的函数重载或 class 模板偏特化。
全面分析
根据 [temp.class.order],我们通过重写重载函数模板来部分排序 class 模板偏特化:
// rewrite corresponding to primary
template<template<typename...> class TTP, typename... Ts>
int f(SS<TTP, Ts...>) { return 0; } // #0
// rewrite corresponding to specialization
template<template<typename> class OneParam, typename... Ts>
int f(SS<OneParam, Ts...>) { return 1; } // #1
int ssoi = f(SS<One, int>{});
正如预期的那样,clang 拒绝了此重写,声称对 f
的调用不明确,MSVC 也是如此; gcc 不一致地拒绝此重写,即使它接受了原始的 class 模板部分专业化。 ICC 接受此重写并将 ssoi
初始化为 1
,对应于 OneParam
专业化 #1
.
现在我们可以根据rules for partial ordering of function templates([temp.func.order])判断哪个编译器是正确的。我们可以看到在 ssoi
初始化时对 f
的调用可以调用 #0
或 #1
,因此要确定哪个更专业,我们必须合成模板参数并尝试执行类型推导:
// #1 -> #0
template<template<typename...> class TTP, typename... Ts>
int f0(SS<TTP, Ts...>);
template<typename> class OneParam1;
int ssoi0 = f0(SS<OneParam1>{});
// #0 -> #1
template<template<typename> class OneParam, typename... Ts>
int f1(SS<OneParam, Ts...>);
template<typename...> class TTP0;
int ssoi1 = f1(SS<TTP0>{});
请注意,根据 [temp.func.order]/5 我们不会合成对应于 Ts...
.
的参数
#1 -> #0
中的推导成功(TTP := OneParam1; Ts... := {}
),正如预期的那样(#1
是对应于 class 模板的部分特化的重写,其中 #0
是重写)。
#0 -> #1
中的推导在 gcc 和 MSVC 下成功(OneParam := TTP0; Ts... := {}
)。 clang(不一致)和 ICC 拒绝 f1
,指出 TTP0
不能推导为 OneParam
(clang:"candidate template ignored: substitution failure : template template argument has different template parameters than its corresponding template template parameter")。
所以首先要判断推演#0 -> #1
是否确实可行。我们可以看一个更简单的等效案例:
template<template<class> class P> class X { };
template<class...> class C { };
X<C> xc;
这被 gcc 接受,被 MSVC(不一致)、clang 和 ICC 拒绝。但是,gcc 接受这个是正确的 [temp.arg.template]/3.
接下来我们必须确定 #1
是否比 #0
更专业,或者它们在排序方面是否有歧义。根据 [temp.deduct.partial]/10 我们依次考虑 #0
和 #1
中的类型;根据 [temp.arg.template]/4 我们可以同时将 OneParam
和 TTP
重写为函数模板:
template<typename...> class X1;
template<typename> class X2;
template<typename PP> int f(X1<PP>); // #2
template<typename... PP> int f(X1<PP...>); // #3
template<typename PP> int g(X2<PP>); // #4
template<typename... PP> int g(X2<PP...>); // #5
我们现在 partial order the rewritten f
and g
overloads, passing through [temp.deduct.partial] and [temp.deduct.type] to determine that per the tie-breaker for variadics #1
比 #0
更专业。
考虑以下代码:
template<typename>
struct One {};
template<typename, typename>
struct Two {};
template<template<typename...> class TTP, typename...>
struct SS;
#ifdef TEST_TTP
template<template<typename> class OneParam,
typename... Ts>
struct SS<OneParam, Ts...> {};
template<template<typename, typename> class TwoParam,
typename... Ts>
struct SS<TwoParam, Ts...> {};
#else // TEST_TTP
template<template<typename> class OneParam,
typename TParam>
struct SS<OneParam, TParam> {};
template<template<typename, typename> class TwoParam,
typename TParam1,
typename TParam2>
struct SS<TwoParam, TParam1, TParam2> {};
#endif // TEST_TTP
int main() {
SS<One, int> ssoi;
SS<Two, int, int> sstii;
}
如果未定义 TEST_TTP
,此代码将在 Clang、GCC 和 MSVC 上正确编译。但是,如果它 是 定义的...
- 代码在 GCC 上正确编译,表明它识别出
OneParam
和TwoParam
与主模板中的TTP
不同。 - Clang 无法识别
OneParam
特化TTP
,导致它发出两个错误(第一个是部分特化没有特化任何模板参数,第二个是OneParam
与之前声明的模板模板参数冲突)。然后它为TwoParam
发出类似的错误(第一个是相同的,而第二个说模板模板参数有太多参数),并为SS
的每个实例化发出一个错误(因为它考虑了模板未定义),总共有 6 个错误。 - MSVC 发出与 Clang 类似的错误,但更简洁:它发出 C3855(
OneParam
与主模板不兼容),并为 [=21= 的每个实例化发出 C2079(变量使用未定义类型) ], 总共有 3 个错误。
现场演示on Coliru.
根据我的测试:
GCC 允许带有模板模板参数的模板采用可变参数包,仅根据模板模板参数采用的参数数量进行部分特化。 Clang 和 MSVC 没有。
template<template<typename...> class T> struct S;
template<template<typename> class T> struct S<T> {}; // Only works with GCC.
template<template<typename, typename> class T> struct S<T> {}; // Only works with GCC.
如果其他参数也专门化,Clang 和 MSVC 也可以。
template<template<typename...> class T, typename... Ts> struct S;
template<template<typename> class T,
typename TParam>
struct S<T, TParam> {};
template<template<typename, typename> class T,
typename TParam1,
typename TParam2>
struct S<T, TParam1, TParam2> {};
因此,前者似乎不是合法的 C++,或者它没有得到 Clang 和 MSVC 的正确支持。所以,问题是:
考虑到这一点,根据模板模板参数采用的参数数量,部分特化包含模板模板参数的模板的正确合法语法是什么?如果没有合法的语法,支持它是 GCC 扩展 and/or 错误吗?
如果需要我执行的测试的完整记录以及提示此问题的原始示例,请查看编辑历史记录。
tl;dr:你的代码是有效的,但没有编译器能正确处理它;即使是那些接受它的人(gcc 和 ICC(英特尔 C++ 编译器))也不一致或出于错误的原因这样做。
正确的推理如下:
- 可变模板模板参数可以be deduced to a single-argument template template parameter, and vice versa。 gcc 是唯一正确的编译器。
- 单参数模板是 more specialized than a variadic template。所有现代编译器都能做到这一点。
- Accordingly,采用单参数模板模板参数的函数模板比采用可变参数模板模板参数的函数模板更专业。似乎唯一正确的编译器是 ICC,但这只是因为它得到 (1) 错误。
- Accordingly,采用单参数模板模板参数的 class 模板比采用可变模板模板参数的 class 模板更专业。 ICC 和 gcc 似乎是正确的,但在前一种情况下,因为它得到 (1) 错误,并且 gcc 与 (3) 不一致。
结论:您使用的是正确的、合法的语法;但是您可能需要针对不兼容的编译器的解决方法。即使您的代码可以编译,我也会 建议使用 static_assert
来验证是否已选择预期的函数重载或 class 模板偏特化。
全面分析
根据 [temp.class.order],我们通过重写重载函数模板来部分排序 class 模板偏特化:
// rewrite corresponding to primary
template<template<typename...> class TTP, typename... Ts>
int f(SS<TTP, Ts...>) { return 0; } // #0
// rewrite corresponding to specialization
template<template<typename> class OneParam, typename... Ts>
int f(SS<OneParam, Ts...>) { return 1; } // #1
int ssoi = f(SS<One, int>{});
正如预期的那样,clang 拒绝了此重写,声称对 f
的调用不明确,MSVC 也是如此; gcc 不一致地拒绝此重写,即使它接受了原始的 class 模板部分专业化。 ICC 接受此重写并将 ssoi
初始化为 1
,对应于 OneParam
专业化 #1
.
现在我们可以根据rules for partial ordering of function templates([temp.func.order])判断哪个编译器是正确的。我们可以看到在 ssoi
初始化时对 f
的调用可以调用 #0
或 #1
,因此要确定哪个更专业,我们必须合成模板参数并尝试执行类型推导:
// #1 -> #0
template<template<typename...> class TTP, typename... Ts>
int f0(SS<TTP, Ts...>);
template<typename> class OneParam1;
int ssoi0 = f0(SS<OneParam1>{});
// #0 -> #1
template<template<typename> class OneParam, typename... Ts>
int f1(SS<OneParam, Ts...>);
template<typename...> class TTP0;
int ssoi1 = f1(SS<TTP0>{});
请注意,根据 [temp.func.order]/5 我们不会合成对应于 Ts...
.
#1 -> #0
中的推导成功(TTP := OneParam1; Ts... := {}
),正如预期的那样(#1
是对应于 class 模板的部分特化的重写,其中 #0
是重写)。
#0 -> #1
中的推导在 gcc 和 MSVC 下成功(OneParam := TTP0; Ts... := {}
)。 clang(不一致)和 ICC 拒绝 f1
,指出 TTP0
不能推导为 OneParam
(clang:"candidate template ignored: substitution failure : template template argument has different template parameters than its corresponding template template parameter")。
所以首先要判断推演#0 -> #1
是否确实可行。我们可以看一个更简单的等效案例:
template<template<class> class P> class X { };
template<class...> class C { };
X<C> xc;
这被 gcc 接受,被 MSVC(不一致)、clang 和 ICC 拒绝。但是,gcc 接受这个是正确的 [temp.arg.template]/3.
接下来我们必须确定 #1
是否比 #0
更专业,或者它们在排序方面是否有歧义。根据 [temp.deduct.partial]/10 我们依次考虑 #0
和 #1
中的类型;根据 [temp.arg.template]/4 我们可以同时将 OneParam
和 TTP
重写为函数模板:
template<typename...> class X1;
template<typename> class X2;
template<typename PP> int f(X1<PP>); // #2
template<typename... PP> int f(X1<PP...>); // #3
template<typename PP> int g(X2<PP>); // #4
template<typename... PP> int g(X2<PP...>); // #5
我们现在 partial order the rewritten f
and g
overloads, passing through [temp.deduct.partial] and [temp.deduct.type] to determine that per the tie-breaker for variadics #1
比 #0
更专业。