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

时间:2015-07-19 02:11:14

标签: c++ templates language-lawyer partial-specialization partial-ordering

根据[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;

相当于在此示例中选择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, "" );

然而,GCC,Clang和ICC都拒绝接受第一个例子,但接受第二个例子。

更奇怪的是,如果将::v替换为::w,则第一个示例有效,反之亦然。未推断出的上下文c::s< c >::显然在专业化排序中被考虑,这是没有意义的。

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

3 个答案:

答案 0 :(得分:7)

暂时转为极端迂腐模式,是的,我认为你在标准中遗漏了一些东西,不,在这种情况下不应该有任何不同。

所有标准参考均为当前工作草案N4527。

[14.5.5.2p1]说:

  

对于两个类模板部分特化,第一个是更多   专门的比第二个if,给出以下重写为2   功能模板,第一个功能模板更专业   根据功能模板的排序规则,比第二个   (14.5.6.2):

     
      
  • 第一个函数模板具有与第一个部分特化相同的模板参数,并且具有单个函数参数   type是一个类模板特化,其模板参数为   第一个部分专业化,和
  •   
  • 第二个函数模板具有与第二个部分特化相同的模板参数,并且具有单个函数参数   其类型是模板的类模板特化   第二部分专业化的论据。
  •   

转到[14.5.6.2p1]:

  

[...] 重载函数模板声明的部分排序   用于以下上下文中以选择函数模板   函数模板专业化指的是:

     
      
  • 在重载解析期间调用函数模板特化(13.3.3);
  •   
  • 当执行功能模板专业化的地址时;
  •   
  • 选择作为功能模板专业化的展示位置运算符删除以匹配展示位置运算符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],它确实会说&#34;其他背景&#34;。我只能得出结论,当将部分排序算法应用于根据[14.5.5.2]中的规则生成的函数模板时,使用函数模板的函数类型,而不是参数类型列表,因为它会发生在函数中。呼叫。

因此,在第一个代码段中选择t的部分特化将不等同于涉及函数调用的情况,而是等同于获取函数模板地址的情况(例如),也属于&#34;其他背景&#34;:

#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>>());
}

(由于我们仍处于极端迂腐模式,我重写了与[14.5.5.2p2]中的示例完全相同的函数模板。)

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

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

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

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

      

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

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

      

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

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

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

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

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

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

演绎算法的核心是DeduceTemplateArgumentsByTypeMatch函数;所有演绎的变体最终都会调用它,然后它会递归地用于遍历复合类型的结构,有时借助于重载的DeduceTemplateArguments函数集,以及一些{{3}根据正在进行的特定演绎类型和正在查看的类型的部分来调整算法。

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

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

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

这是功能模板的部分排序。基于上一节中引用的段落,我期望类模板特化的部分排序,用适当构造的函数模板和TPOC_Other标志调用isAtLeastAsSpecializedAs,并从那里自然流动。如果是这种情况,那么您的示例应该可行。惊喜:事情并非如此。

类模板特化的部分排序从Sema::getMoreSpecializedTemplate开始。作为优化(红旗!),它不会合成函数模板,而是使用Sema::getMoreSpecializedPartialSpecialization直接对类模板特化本身进行类型推导,作为P和{{的类型1}}。这可以;毕竟,无论如何,功能模板的算法最终会做什么。

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

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

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

作为旁注,我认为所有这些都适用于issue 549

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

答案 1 :(得分:1)

此答案中的信息很大程度上取决于this question。模板部分排序算法未在标准中指定。主要的编译器似乎至少同意算法应该是什么。

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

template <typename c>
constexpr int f( t<c> ) { return 0; } 

函数调用也变得模棱两可。其原因是部分排序类型合成算法不实例化模板而是合成新的唯一类型。

首先,如果我们比较刚才介绍的函数:

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

我们有:

+---+---------------------+----------------------+
|   | Parameters          | Arguments            |
+---+---------------------+----------------------+
| 0 | c, typename c::v    | Unique0, void        |
| 1 | c, void             | Unique1, Unique1_v   |
+---+---------------------+----------------------+

我们会忽略部分排序扣除规则中未推断的上下文,因此Unique0匹配c,但Unique1_vvoid不匹配!因此0是首选。这可能不是你所期望的。

如果我们然后比较02

+---+--------------------------+----------------------+
|   | Parameters               | Arguments            |
+---+--------------------------+----------------------+
| 0 | s<c>, typename s<c>::w   | Unique0, void        |
| 2 | c, void                  | Unique2, Unique2_v   |
+---+--------------------------+----------------------+

此处0扣除失败(因为Unique0s<c>不匹配),但2扣除也失败(因为Unique2_v赢了' t匹配void)。这就是为什么它模棱两可。

这引出了一个关于void_t的有趣问题:

template <typename... >
using void_t = void;

此函数重载:

template< typename c >
constexpr int f( t< s< c >, void_t<s<c>>> ) { return 3; }

优先于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; }

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

答案 2 :(得分:1)

我觉得意图是编译的例子,但是,标准并没有清楚地说明当部分排序使用的合成参数列表的模板参数列表匹配时应该发生什么(如果有的话)(14.5.5.1) / 1):

  

这是通过将类模板特化的模板参数与部分特化的模板参数列表进行匹配来完成的。

以上段落是必需的,以便在以下选择#1:

template <typename T, typename Q> struct A;
template <typename T>             struct A<T, void> {}; #1
template <typename T>             struct A<T, char> {}; #2

void foo ()
{
  A<int, void> a;
}

下面:

  1. 模板参数T推断为int(14.5.5.1/2)
  2. 结果参数列表匹配:int == intvoid == void(14.5.5.1/1)
  3. 对于部分订购案例:

    template< typename c > struct t< c, typename c::v > {};  #3
    template< typename c > struct t< s< c >, typename s< c >::w > {}; #4
    

    对于第一个参数,#4更专业,第二个参数都是非推导的上下文,即。类型推导成功#4到#3但不适用#3到#4。

    我认为编译器正在应用&#34;参数列表必须匹配&#34;合成参数列表中的14.5.5.1/1规则。这会将第一个合成类型Q1::v与第二个s<Q2>::w进行比较,但这些类型并不相同。

    这可以解释为什么将v更改为w会导致一些示例正常工作,因为编译器决定这些类型是相同的。

    这不是部分排序之外的问题,因为类型是具体的,因为像c::v这样的类型将被实例化为void等。

    可能是委员会打算申请类型等同(14.4),但我不这么认为。该标准应该只是明确说明在部分排序步骤中创建的合成类型的匹配(或不匹配)应该发生什么。