【问题标题】:What is the syntax for partially specialising a template based on the number of parameters a template template parameter takes?根据模板模板参数采用的参数数量,部分特化模板的语法是什么?
【发布时间】:2017-03-30 02:39:45
【问题描述】:

考虑以下代码:

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 上正确编译,表明它识别出 OneParamTwoParam 与主模板中的 TTP 不同。
  • Clang 无法识别 OneParam 专门化 TTP,导致它发出两个错误(第一个是部分专门化没有专门化任何模板参数,第二个是 OneParam 与之前的冲突-声明的模板模板参数)。然后它为TwoParam 发出类似的错误(第一个是相同的,而第二个说模板模板参数有太多参数),并且SS 的每个实例化都一个错误(因为它认为模板未定义) ,总共有 6 个错误。
  • MSVC 发出与 Clang 类似的错误,但更简洁:它发出 C3855(OneParam 与主模板不兼容)和一个 C2079(变量使用未定义类型),用于 SS 的每个实例化,总共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 扩展和/或错误?

如果需要我执行的测试的完整记录以及提示此问题的原始示例,请查看编辑历史记录。

【问题讨论】:

    标签: c++ visual-c++ g++ clang++ template-templates


    【解决方案1】:

    tl;dr:您的代码是有效的,但没有编译器正确处理它;即使是那些接受它的人(gcc 和 ICC(英特尔 C++ 编译器))也不一致或出于错误的原因。

    正确推理如下:

    1. 可变参数模板模板参数可以be deduced to a single-argument template template parameter, and vice versa。 gcc 是唯一能做到这一点的编译器。
    2. 单参数模板是more specialized than a variadic template。所有现代编译器都做到了这一点。
    3. Accordingly,采用单参数模板模板参数的函数模板比​​采用可变参数模板模板参数的函数模板更专业。唯一看起来正确的编译器是 ICC,但这只是因为它 (1) 错误。
    4. Accordingly,采用单参数模板模板参数的类模板比采用可变参数模板模板参数的类模板更专业。 ICC 和 gcc 似乎得到了这个正确,但在前一种情况下,因为它得到 (1) 错误,并且 gcc 与 (3) 不一致。

    结论:您使用的是正确、合法的语法;但是您可能需要针对不兼容的编译器的解决方法。即使您的代码编译完成,我也会建议使用static_assert 来验证是否选择了预期的函数重载或类模板部分特化。


    全面分析

    根据[temp.class.order],我们通过重写重载函数模板来对类模板部分特化进行部分排序:

    // 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 接受了原始类模板的部分特化,它也不一致地拒绝这种重写。 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 -&gt; #0 中的推导成功(TTP := OneParam1; Ts... := {}),正如预期的那样(#1 是对应于类模板的部分特化的重写,#0 是重写)。

    在 gcc 和 MSVC 下#0 -&gt; #1 的推演成功(OneParam := TTP0; Ts... := {})。 clang(不一致)和ICC拒绝f1,指出TTP0不能推导出为OneParam(clang:“候选模板被忽略:替换失败:模板模板参数的模板参数与其对应的模板模板参数不同”)。

    所以我们首先要确定推演#0 -&gt; #1是否确实可行。我们可以看一个更简单的等价案例:

    template<template<class> class P> class X { };
    template<class...> class C { };
    X<C> xc;
    

    这被 gcc 接受,并被 MSVC(不一致)、clang 和 ICC 拒绝。但是,按照[temp.arg.template]/3,gcc 接受这个是正确的。

    接下来我们必须确定#1是否比#0更专业,或者它们在排序方面是否有歧义。根据[temp.deduct.partial]/10我们依次考虑#0#1中的类型;根据 [temp.arg.template]/4,我们可以同时将 OneParamTTP 重写为函数模板:

    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 重写了 fg 重载,通过 [temp.deduct.partial][temp.deduct.type]确定每个 tie-breaker for variadics #1#0 更专业。

    【讨论】:

    • 这是一个非常深入的答案。我什至没有想过尝试 ICC 或其他编译器,我非常感谢始终使用 static_assert 验证它的建议,因为编译器遇到了很多麻烦。非常感谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-05-10
    • 2015-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多