9

根据 [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 >::是在专业化排序中考虑的,这是没有意义的。

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

4

3 回答 3

8

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

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

[14.5.5.2p1] 说:

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

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

前往 [14.5.6.2p1]:

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

  • 在调用函数模板特化 (13.3.3) 的重载决议期间;
  • 当一个函数模板特化的地址被取走时;
  • when a placement operator delete that is a function template specialization is selected to match a placement operator 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>>());
}

(由于我们仍处于极度迂腐模式,我重写了函数模板,就像 [14.5.5.2p2] 中的示例一样。)

不用说,这也编译和打印t<s<C>, s<C>::w>。它产生不同行为的机会很小,但我必须尝试一下。考虑到算法是如何工作的,如果函数参数是引用类型(在函数调用的情况下触发 [14.8.2.4] 中的特殊规则,但在其他情况下不触发),将会有所不同,但是这样的形式不能出现在类模板特化生成的函数模板中。

所以,这整个弯路对我们没有一点帮助,但是......这是一个language-lawyer问题,我们必须在这里有一些标准的报价......


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

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

    模板参数推导是尝试匹配 aP和 a deduced AP但是,如果和推导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_SuccessisAtLeastAsSpecializedAs则只进行一次检查,以验证用于部分排序的所有模板参数是否具有值。如果这也很好,它会返回true

这就是函数模板的部分排序。根据上一节中引用的段落,我期望类模板专业化的部分排序可以Sema::getMoreSpecializedTemplate使用适当构造的函数模板和 的标志来调用TPOC_Other,并且一切都会从那里自然流动。如果是这种情况,您的示例应该有效。惊喜:事实并非如此。

类模板特化的部分排序从Sema::getMoreSpecializedPartialSpecialization. 作为一种优化(危险信号!),它不合成函数模板,而是DeduceTemplateArgumentsByTypeMatch直接在类模板特化本身上进行类型推导,作为Pand的类型A。这可以; 毕竟,这就是函数模板的算法最终会做的事情。

但是,如果在推导过程中一切顺利,它就会调用FinishTemplateArgumentDeduction(类模板特化的重载),它会进行替换和其他检查,包括检查特化的替换参数是否与原始参数等价。如果代码正在检查部分特化是否与一组参数匹配,这会很好,但在部分排序期间并不好,并且据我所知,这会导致您的示例出现问题。

所以,看起来理查德科登关于发生的事情的假设是正确的,但我不完全确定这是故意的。这对我来说更像是一个疏忽。我们如何最终使所有编译器都以相同的方式运行仍然是一个谜。

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

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

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

于 2015-07-30T22:28:13.810 回答
2

我觉得目的是示例编译,但是,标准没有明确说明在为部分排序(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== int, void== void(14.5.5.1/1)

对于偏序情况:

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 的类型推导不成功。

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

这可以解释为什么更改vw导致一些示例工作,因为编译器认为这些类型是相同的。

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

可能是委员会打算应用类型等效(14.4),但我不这么认为。该标准可能应该准确地阐明作为部分排序步骤的一部分创建的合成类型的匹配(或不匹配)应该发生什么。

于 2015-07-23T18:09:14.087 回答
1

此答案中的信息在很大程度上基于此问题。模板部分排序算法没有被标准指定。主要编译器似乎至少同意算法应该是什么。


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

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_v不匹配void!因此0是优选的。这可能不是您所期望的。

如果我们然后比较0and 2

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

在这里,0扣除失败(因为Unique0不会匹配s<c>),但2扣除也失败(因为Unique2_v不会匹配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

于 2015-07-19T09:39:32.880 回答