使用非推导上下文的部分特化排序

Pot*_*ter 8 c++ templates partial-specialization partial-ordering language-lawyer

根据[temp.class.order]§14.5.5.2,t在此示例中选择部分特化:

template< typename >
struct s { typedef void v, w; };

template< typename, typename = void >
struct t {};

template< typename c >
struct t< c, typename c::v > {};

template< typename c >
struct t< s< c >, typename s< c >::w > {};

t< s< int > > q;
Run Code Online (Sandbox Code Playgroud)

等效f于此示例中的重载选择:

template< typename >
struct s { typedef void v, w; };

template< typename, typename = void >
struct t {};

template< typename c >
constexpr int f( t< c, typename c::v > ) { return 1; }

template< typename c >
constexpr int f( t< s< c >, typename s< c >::w > ) { return 2; }

static_assert ( f( t< s< int > >() ) == 2, "" );
Run Code Online (Sandbox Code Playgroud)

但是,GCC,Clang和ICC都认为第一个例子含糊不清,但接受第二个例子.

更奇怪的是,第一个例子在::v被替换时起作用,::w反之亦然.非推断上下文c::s< c >::显然正在专业化排序,这是没有意义的考虑.

我是否遗漏了标准中的内容,或者所有这些实现都有相同的错误?

bog*_*dan 7

暂时切换到极端迂腐模式,是的,我认为你在标准中遗漏了一些东西,不,它在这种情况下应该没有任何区别.

所有标准参考均为N4527,即当前的工作草案.

[14.5.5.2p1]说:

对于两个类模板部分特化,第一个比第二个更专业,如果给定以下两个函数模板的重写,第一个函数模板比​​第二个更加专业化,根据函数模板的排序规则(14.5.6.2):

  • 第一个函数模板具有与第一个部分特化相同的模板参数,并且具有单个函数参数,其类型是具有第一个部分特化的模板参数的类模板特化,并且
  • 第二个函数模板具有与第二个部分特化相同的模板参数,并且具有单个函数参数,其类型是具有第二部分特化的模板参数的类模板特化.

转到[14.5.6.2p1]:

[...] 在以下上下文中使用重载函数模板声明的部分排序来选择函数模板特化引用的函数模板:

  • 在重载解析期间调用函数模板特化(13.3.3);
  • 当采用函数模板专业化的地址时;
  • 当选择位置运算符delete作为函数模板特化时,匹配一个贴图运算符new(3.7.4.2,5.3.4);
  • 当友元函数声明(14.5.4),显式实例化(14.7.2)或显式特化(14.7.3)引用函数模板特化时.

没有提到类模板特化的部分排序.但是,[14.8.2.4p3]说:

用于确定排序的类型取决于完成部分排序的上下文:

  • 在函数调用的上下文中,使用的类型是函数调用具有参数的函数参数类型.
  • 在调用转换函数的上下文中,使用转换函数模板的返回类型.
  • 在其他上下文(14.5.6.2)中,使用了函数模板的函数类型.

尽管它回溯到[14.5.6.2],但它确实说"其他情境".我只能得出结论,当将部分排序算法应用于根据[14.5.5.2]中的规则生成的函数模板时,使用函数模板的函数类型,而不是参数类型列表,因为它会发生在函数中.呼叫.

因此,t在第一个片段中选择部分特化将不等同于涉及函数调用的情况,而是与获取函数模板(例如)的地址相同,也属于"其他上下文":

#include <iostream>

template<typename> struct s { typedef void v, w; };
template<typename, typename = void> struct t { };

template<typename C> void f(t<C, typename C::v>) { std::cout << "t<C, C::v>\n"; }
template<typename C> void f(t<s<C>, typename s<C>::w>) { std::cout << "t<s<C>, s<C>::w>\n"; }

int main()
{
   using pft = void (*)(t<s<int>>);
   pft p = f;
   p(t<s<int>>());
}
Run Code Online (Sandbox Code Playgroud)

(因为我们仍处于极端迂腐模式,所以我重写了函数模板,就像[14.5.5.2p2]中的例子一样.)

不用说,这也编译和打印t<s<C>, s<C>::w>.它产生不同行为的可能性很小,但我不得不尝试.考虑到算法如何工作,如果函数参数是例如引用类型(在函数调用的情况下触发[14.8.2.4]中的特殊规则,而在其他情况下不触发),则会产生差异,但是使用从类模板特化生成的函数模板不会出现这种形式.

所以,这整个绕道并没有帮助我们一点,但是......这是一个language-lawyer问题,我们必须在这里有一些标准的引用......


有一些与您的示例相关的活动核心问题:

  • 1157包含一个我认为相关的注释:

    模板参数推导是一种匹配a P和推导 的尝试A; 但是,如果P和推导出的模板参数推断A不兼容,则不会指定模板参数推断失败.这可能在存在未推断的上下文的情况下发生.尽管在14.8.2.4 [temp.deduct.partial]第9段中有括号声明,但模板参数推导可能成功地确定每个模板参数的模板参数,同时产生A与相应的模板参数不兼容的推导P.

    我不完全确定这么清楚; 毕竟,[14.8.2.5p1]说

    [...]找到模板参数值[...],在替换推导出的值[...]后,将使P与A兼容.

    和[14.8.2.4]完整提及[14.8.2.5].但是,很明显,当涉及非推断的上下文时,函数模板的部分排序不会寻求兼容性,并且更改它会破坏许多有效的情况,所以我认为这只是标准中缺乏适当的规范.

  • 在较小的程度上,1847与模板特化的参数中出现的非推导的上下文有关.它提到1391的决议; 我认为这个措辞存在一些问题 - 这个答案中有更多细节.

对我而言,所有这些都说明你的榜样应该有效.


和你一样,我对三个不同的编译器中存在相同的不一致这一事实很感兴趣.在我确认MSVC 14表现出与其他人完全相同的行为后,我更加感兴趣.所以,当我有一段时间,我想我会快速看看Clang做了什么; 事实证明它不是很快,但它产生了一些答案.

所有与我们案例相关的代码都在lib/Sema/SemaTemplateDeduction.cpp.

演绎算法的核心是DeduceTemplateArgumentsByTypeMatch函数; 所有演绎的变体最终都会调用它,然后它会递归地用于遍历复合类型的结构,有时借助于重载DeduceTemplateArguments的函数集,以及一些标志来根据特定的演绎类型调整算法完成并查看类型表单的各个部分.

关于此功能需要注意的一个重要方面是它处理严格的扣除,而不是替换.它比较类型表单,推导出在推导的上下文中出现的模板参数的模板参数值,并跳过非推导的上下文.它唯一的其他检查是验证模板参数的推导参数值是否一致.在上面提到的答案中,我已经写了更多关于Clang在部分排序期间做出演绎的方式.

对于函数模板的部分排序,算法从Sema::getMoreSpecializedTemplate成员函数开始,该函数使用类型的标志enum TPOC来确定正在进行部分排序的上下文; 枚举是TPOC_Call,TPOC_Conversion,和TPOC_Other; 不言自明的.然后isAtLeastAsSpecializedAs,此函数在两个模板之间来回调用两次,并比较结果.

isAtLeastAsSpecializedAs打开TPOC标志的值,根据该值进行一些调整,并最终直接或间接地调用DeduceTemplateArgumentsByTypeMatch.如果返回Sema::TDK_Success,isAtLeastAsSpecializedAs则仅进行一次检查,以验证用于部分排序的所有模板参数都具有值.如果这也很好,它会返回true.

这是功能模板的部分排序.基于上一节中引用的段落,我期待类模板特化的部分排序,以Sema::getMoreSpecializedTemplate使用适当构造的函数模板和标志来调用TPOC_Other,并且从那里自然流动的一切.如果是这种情况,那么您的示例应该可行.惊喜:那不是发生了什么.

类模板特化的部分排序开始于Sema::getMoreSpecializedPartialSpecialization.作为一种优化(红旗!),它并不能合成功能模板,而是采用DeduceTemplateArgumentsByTypeMatch直接在类模板做类型推导专业化作为自己的类型PA.这可以; 毕竟,这就是功能模板的算法最终会做的事情.

但是,如果在演绎期间一切顺利,它会调用FinishTemplateArgumentDeduction(类模板特化的重载),它会进行替换和其他检查,包括检查特化的替换参数是否与原始参数相同.如果代码检查部分特化是否与一组参数匹配,但在部分排序期间不正常,并且据我所知,这会导致问题与您的示例相关.

所以,理查德科登关于所发生事情假设似乎是正确的,但我并不完全确定这是故意的.这看起来更像是对我的疏忽.我们如何最终得到所有编译器的行为方式仍然是个谜.

在我看来,删除两个FinishTemplateArgumentDeduction来自from的调用Sema::getMoreSpecializedPartialSpecialization不会造成任何伤害,并且会恢复部分排序算法的一致性.不需要额外检查(完成isAtLeastAsSpecializedAs)所有模板参数都具有值,因为我们知道所有模板参数都可以从特化参数中推导出来; 如果它们不是,则部分特化将失败匹配,因此我们不会首先进行部分排序.(首先是否允许这种部分专业化是问题549的主题.Clang对此类案件发出警告,MSVC和GCC发出错误.无论如何,这不是问题.)

作为旁注,我认为所有这些都适用于变量模板特化重载.

不幸的是,我没有为Clang设置构建环境,因此我暂时无法测试此更改.


Bar*_*rry 1

此答案中的信息很大程度上基于此问题。标准未指定模板部分排序算法。主要编译器似乎至少就算法应该是什么达成了一致。


首先,你的两个例子并不等同。除了主模板之外,您还有两个模板专业化,但对于您的函数示例,您不会为主模板添加函数重载。如果添加它:

template <typename c>
constexpr int f( t<c> ) { return 0; } 
Run Code Online (Sandbox Code Playgroud)

函数调用也变得不明确。原因是偏序类型合成算法不会实例化模板,而是合成新的唯一类型。

首先,如果我们将我刚刚介绍的函数与这个函数进行比较:

template< typename c >
constexpr int f( t< c, typename c::v > ) { return 1; }
Run Code Online (Sandbox Code Playgroud)

我们有:

+---+---------------------+----------------------+
|   | Parameters          | Arguments            |
+---+---------------------+----------------------+
| 0 | c, typename c::v    | Unique0, void        |
| 1 | c, void             | Unique1, Unique1_v   |
+---+---------------------+----------------------+
Run Code Online (Sandbox Code Playgroud)

我们忽略偏序推导规则中的非推导上下文,因此Unique0匹配c,但Unique1_v不匹配void!因此0是优选的。这可能不是您所期望的。

如果我们然后比较02

+---+--------------------------+----------------------+
|   | Parameters               | Arguments            |
+---+--------------------------+----------------------+
| 0 | s<c>, typename s<c>::w   | Unique0, void        |
| 2 | c, void                  | Unique2, Unique2_v   |
+---+--------------------------+----------------------+
Run Code Online (Sandbox Code Playgroud)

这里,0推导失败(since Unique0won't match s<c>),但2推导也失败(since Unique2_vwon't match void)。这就是为什么它是暧昧的。


这让我想到了一个有趣的问题void_t

template <typename... >
using void_t = void;
Run Code Online (Sandbox Code Playgroud)

该函数重载:

template< typename c >
constexpr int f( t< s< c >, void_t<s<c>>> ) { return 3; }
Run Code Online (Sandbox Code Playgroud)

会优先于0,因为参数是s<c>void。但这不会是:

template <typename... >
struct make_void {
    using type = void;
};

template< typename c >
constexpr int f( t< s< c >, typename make_void<s<c>>::type> ) { return 4; }
Run Code Online (Sandbox Code Playgroud)

由于我们不会实例化make_void<s<c>>来确定::type,因此我们最终会遇到与 相同的情况2