20

考虑以下简单(就模板问题而言)示例:

#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.deduct.partial]和[temp.func.order]中的规则,要确定偏序,我们需要合成一些唯一的类型。所以我们有两次推演尝试:

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

对于“b”的推论,根据Richard Corden 的回答,表达式typename identity<UniqueB>::type被视为一种类型并且不被评估。也就是说,这将被合成为:

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

很明显,对“b”的推论失败了。这是两种不同的类型,因此您不能同时推断出这两种类型T

但是,在我看来,扣除A应该失败。对于第一个参数,您将匹配T == UniqueA. 第二个论点是一个非演绎的上下文 - 那么如果UniqueA可以转换为,那么演绎不会成功identity<UniqueA>::type吗?后者是替换失败,所以我看不出这个推论怎么会成功。

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

4

2 回答 2

23

正如评论中所讨论的,我相信函数模板部分排序算法的几个方面在标准中不清楚或根本没有指定,这在您的示例中显示。

为了让事情变得更有趣,MSVC(我测试了 12 和 14)拒绝了这个不明确的调用。我认为标准中没有任何内容可以最终证明哪个编译器是正确的,但我想我可能知道差异来自哪里;下面有一个说明。

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


首先,针对问题 1391的提议决议。我们在评论和聊天中广泛讨论了它。我认为,虽然它确实提供了一些澄清,但它也引入了一些问题。它将 [14.8.2.4p4] 更改为(新文本以粗体显示):

上面从参数模板中指定的每种类型和参数模板中的相应类型都用作 和 的 P类型A如果一个特定P的不包含参与模板参数推导的模板参数,则不P用于确定排序。

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

  • ifP是非依赖的,它根本不包含任何模板参数,因此它也不包含任何参与参数推导的参数,这将使粗体语句适用于它。template<class T> f(T, int)但是,这将是template<class T, class U> f(T, U)无序的,这是没有意义的。这可以说是对措辞的解释问题,但可能会引起混淆。
  • 它与用于确定 ordering的概念混淆,这会影响 [14.8.2.4p11]。这使得template<class T> void f(T)template<class T> void f(typename A<T>::a)无序(从第一个到第二个推导成功,因为T没有用于根据新规则进行偏序的类型,所以它可以保持没有值)。目前,我测试过的所有编译器都报告第二个更专业。
  • 它会#2#1以下示例更专业:

    #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的第二个参数不用于偏序,因此从#1to推导成功,反之则#2不行)。目前,这个电话是模棱两可的,可以说应该保持这样。


在查看了 Clang 对部分排序算法的实现之后,我认为可以更改标准文本以反映实际发生的情况。

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

对于P/A对:

  • ifP是非依赖的,当且仅当PA是同一类型时,推论才被认为是成功的。
  • 不执行将推导的模板参数替换为出现的非推导上下文P并且不影响推导过程的结果。
  • 如果模板实参值被成功推导出P除了那些只出现在非推导上下文中的模板参数,那么推导被认为是成功的(即使某些参数在P推导过程结束时仍然没有值)P/A对)。

笔记:

  • 关于第二个要点:[14.8.2.5p1] 讨论了查找模板参数值,这些值将使P. 在替换推导值(称为推导值A)后,与A. 这可能会导致混淆部分排序期间实际发生的情况;没有替换发生。
  • 在某些情况下,MSVC 似乎没有实现第三个要点。有关详细信息,请参阅下一节。
  • 第二个和第三个要点还旨在涵盖P具有类似 形式的情况A<T, typename U::b>,这些情况未包含在 issue 1391 中的措辞中。

将当前 [p10] 更改为:

当且仅当以下情况时,函数模板F至少与函数模板一样专业化 :G

  • 对于用于确定排序的每一对类型, from 的类型F至少与 from 的类型一样特化G,并且,
  • 当使用转换F的作为参数模板和参数模板执行推导时G,在对所有类型对进行推导之后,用于确定排序的类型 G中使用的所有模板参数都有值,并且这些值在整个所有类型对。

F Gif更专业化F至少和 一样专业化,GG至少不像F.

使整个当前 [p11] 成为一个音符。

(由 1391 到 [14.8.2.5p4] 的分辨率添加的注释也需要调整 - 对于 [14.8.2.1] 可以,但对于 [14.8.2.4] 则不行。)


对于 MSVC,在某些情况下,看起来所有模板参数都P需要在该特定P/A的推演期间接收值,以便推演成功 from Ato P。我认为这可能是导致您的示例和其他示例中的实现分歧的原因,但我已经看到至少一种情况,上述情况似乎并不适用,所以我不确定该相信什么。

上面的语句似乎确实适用的另一个示例:在您的示例中更改template<typename T> void bar(T, T)template<typename T, typename U> void bar(T, U)交换结果:调用在 Clang 和 GCC 中不明确,但b在 MSVC 中解析为。

一个没有的例子:

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

正如预期的那样,这#2在 Clang 和 GCC 中进行了选择,但 MSVC 将调用拒绝为模棱两可;不知道为什么。


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

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

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

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

于 2015-07-30T22:26:50.317 回答
5

我认为关键在于以下声明:

第二个参数是一个非推导的上下文——那么如果 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 用于确定哪些功能更专业。

于 2015-07-22T09:21:14.663 回答