提问者:小点点

模板部分排序-为什么部分演绎在这里成功


考虑下面的简单(就模板问题而言):

#include <iostream>

template <typename T>
struct identity;

template <>
struct identity<int> {
    using type = int;
};

template<typename T> void bar(T, T ) { std::cout << "a\n"; }
template<typename T> void bar(T, typename identity<T>::type) { std::cout << "b\n"; }

int main ()
{
    bar(0, 0);
}

clang和gcc都在那里打印“a”。根据[temp.Decrete.partial]和[temp.func.order]中的规则,为了确定偏序,我们需要合成一些唯一的类型。因此,我们有两种演绎尝试:

+---+-------------------------------+-------------------------------------------+
|   | Parameters                    | Arguments                                 |
+---+-------------------------------+-------------------------------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA                          |
| b | T, T                          | UniqueB, typename identity<UniqueB>::type |
+---+-------------------------------+-------------------------------------------+

根据Richard Corden的回答,对于“b”的推论,表达式typename identity

+---+-------------------------------+--------------------+
|   | Parameters                    | Arguments          |
+---+-------------------------------+--------------------+
| a | T, typename identity<T>::type | UniqueA, UniqueA   |
| b | T, T                          | UniqueB, UniqueB_2 |
+---+-------------------------------+--------------------+

很明显,“b”的扣减是失败的。这是两种不同的类型,因此您无法同时推断T

然而,在我看来,对A的演绎应该失败。对于第一个参数,您需要匹配T==UniqueA。第二个参数是一个非推导的上下文——所以如果UniqueA可以转换为标识,推导不会成功吗

在这种情况下,gcc和clang如何以及为什么更喜欢“a”重载?


共2个答案

匿名用户

正如在评论中所讨论的,我认为函数模板偏序算法有几个方面在标准中不清楚或根本没有规定,这在您的示例中显示出来。

为了让事情变得更有趣,MSVC(我测试了12和14)拒绝了这个模棱两可的调用。我不认为标准中有任何东西可以决定性地证明哪个编译器是正确的,但我想我可能有一个关于差异来源的线索;下面有一个关于这个的注释。

你的问题(和这个问题)让我对事情是如何运作的进行更多的调查。我决定写这个答案不是因为我认为它是权威的,而是为了把我在一个地方找到的信息组织起来(它不适合评论)。我希望它会有用。

第一,第1391号问题的拟议决议。我们在评论和聊天中进行了广泛的讨论。我认为,虽然它确实提供了一些澄清,但也提出了一些问题。将[14.8.2.4p4]更改为(新的粗体文本):

以上从参数模板中指定的每种类型以及从参数模板中指定的相应类型都用作PA的类型。如果特定的P不包含参与模板参数推导的模板参数,则该P不用于确定顺序。

在我看来,这不是一个好主意,原因有几个:

>

#include <iostream>

template<class T> struct A { using a = T; };

struct D { };
template<class T> struct B { B() = default; B(D) { } };
template<class T> struct C { C() = default; C(D) { } };

template<class T> void f(T, B<T>) { std::cout << "#1\n"; } // #1
template<class T> void f(T, C<typename A<T>::a>) { std::cout << "#2\n"; } // #2

int main()
{
   f<int>(1, D());
}

#2的第二个参数不用于部分排序,所以从#1#2的推导会成功,但不会反过来)。目前,这一呼吁是模棱两可的,可以说应该保持模棱两可。

在看了Clang对部分排序算法的实现后,以下是我认为标准文本可以如何改变以反映实际发生的事情。

保持[p4]不变,并在[p8]和[p9]之间添加以下内容:

对于P/A对:

  • 如果P是非依赖的,则当且仅当PA是同一类型时,扣减才被视为成功
  • 将推导模板参数替换到P中出现的非推导上下文中不会执行,也不会影响推导过程的结果
  • 如果成功为P的所有模板参数推导模板参数值,但仅在非推导上下文中出现的模板参数除外,然后,推断被认为是成功的(即使P中使用的某些参数在该特定P/a对的推断过程结束时仍然没有值)

笔记:

  • 关于第二个要点:[14.8.2.5p1]谈到寻找模板参数值,这些参数值将使推导值替换后的P(称之为推导的A)与A兼容。这可能会导致对部分排序期间实际发生的事情的混淆;没有替代正在进行。
  • MSVC在某些情况下似乎没有实现第三个要点。有关详细信息,请参阅下一节。
  • 第二和第三个要点旨在涵盖P具有A等形式的情况

将电流[p10]更改为:

函数模板F至少和函数模板G一样专用,当且仅当:

  • 对于用于确定排序的每一对类型,来自F的类型至少与来自G的类型一样专门,并且,
  • 当使用转换后的F作为参数模板和G作为参数模板执行推导时,在对所有类型进行推导后,从G具有值,并且这些值在所有类型对中都是一致的。

FG更专业,如果F至少和G一样专业,并且G至少没有F那么专业。

把整个电流[p11]记下来。

(1391决议添加到[14.8.2.5p4]的注释也需要调整-对于[14.8.2.1]来说可以,但是对于[14.8.2.4]来说不行。)

对于MSVC,在某些情况下,P中的所有模板参数似乎都需要在该特定P/A对的演绎过程中接收值,以便从AP的演绎成功。我认为这可能是在您的示例和其他示例中导致实现差异的原因,但我至少看到过一种情况,上面的内容似乎不适用,因此我不确定该相信什么。

上面的语句似乎适用的另一个例子是:更改模板

一个不适用的示例:

#include <iostream>

template<class T> struct A { using a = T; };
template<class, class> struct B { };

template<class T, class U> void f(B<U, T>) { std::cout << "#1\n"; }
template<class T, class U> void f(B<U, typename A<T>::a>) { std::cout << "#2\n"; }

int main()
{
   f<int>(B<int, int>());
}

正如预期的那样,这会在Clang和GCC中选择#2,但MSVC拒绝了该调用,因为该调用不明确;不知道为什么。

标准中描述的部分排序算法谈到合成一个唯一的类型、值或类模板来生成参数。克拉克通过...不合成任何东西来做到这一点。它只是使用依赖类型的原始形式(如声明的),并以两种方式匹配它们。这是有道理的,因为替换合成类型不会添加任何新信息。它不能更改A类型的形式,因为通常没有办法知道替换的形式可以解析到哪些具体类型。合成的类型未知,这使得它们与模板参数非常相似。

当遇到一个P是非推断上下文时,Clang的模板参数推断算法会跳过它,为该特定步骤返回“success”。这不仅发生在偏序期间,而且发生在所有类型的演绎中,不仅发生在函数参数列表的顶层,而且在遇到复合类型形式的非演绎上下文时递归地发生。出于某种原因,我第一次看到它时就感到惊讶。仔细想想,它当然是有道理的,而且符合标准([…]不参与类型扣减[…]在[14.8.2.5p4]中。

这与Richard Corden对其答案的评论是一致的,但我必须实际查看编译器代码才能理解所有含义(这不是他的答案的错误,而是我自己的错误——程序员在代码和其他方面的思考)。

我在这个答案中包含了更多关于Clang实现的信息。

匿名用户

我认为关键是以下声明:

第二个参数是一个未推导的上下文-如果UniqueA可转换为identity::type,那么推导不会成功吗?

类型推断不执行“转换”检查。作为重载解析的一部分,这些检查使用真实的显式和推导的参数进行。

这是我对选择要调用的函数模板所采取步骤的总结(所有引用均取自N3937,~C'14):

  1. 显式参数被替换,结果函数类型检查是否有效。(14.8.2/2)
  2. 执行类型推导,并替换推导出的参数。同样,结果类型必须有效。(14.8.2/5)
  3. 在步骤1和2中成功的函数模板是专门化的,并包含在重载集中以解决重载问题。(14.8.3/1)
  4. 通过重载解析比较转换序列。(13.3.3)
  5. 如果两个函数专门化的转换序列不是更好,则使用部分排序算法来查找更专门化的函数模板。(13.3.3)
  6. 部分排序算法只检查成功的类型推导。(14.5.6.2/2)

编译器在步骤4中已经知道,当使用真实参数时,可以调用这两个专门化。正在使用步骤5和步骤6来确定哪些职能更加专门化。