【问题标题】:Template partial ordering - why does partial deduction succeed here模板偏序——为什么这里偏扣成功
【发布时间】:2015-10-02 08:38:54
【问题描述】:

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

#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's answer,表达式typename identity&lt;UniqueB&gt;::type被视为一个类型并且不被计算。也就是说,这将被合成为:

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

很明显,对“b”的推论失败了。这是两种不同的类型,因此您无法将T 推断为两者。

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

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

【问题讨论】:

  • stackoverflow.com/a/1182688/4326278 包含相关信息,我认为。
  • @bogdan 从将typename identity&lt;UniqueB&gt;::type 视为UniqueB_2 的角度来看,这是有道理的。但是为什么对“a”的扣除会成功呢?我稍微改写了这个问题。
  • 据我所知,这不仅适用于生成唯一参数,还适用于调整参数:T, typename identity&lt;T&gt;::type 变为T, U,因此推演成功。然而,该算法显然还做了一些标准中未指定的事情。在这种情况下,它“记住”T 实际上是在 U 替换的构造中使用的,因此,在推演过程结束时,如果 T 尚未从其他地方推演,推演仍然失败总体而言(在 [14.8.2.4p11] 中有提示)。
  • 这就是为什么在 the other question 的示例中,仅使用一个参数(bar(T)bar(typename identity&lt;T&gt;::type))进行推导失败的原因。正如我所说,整个事情都没有详细说明。 Issue 1391 可能会改变对非推断上下文的处理,并让事情变得更好,尽管还远非完美。
  • 附带说明,另一个未指定的情况是为什么template&lt;typename T&gt; void f(T, T)template&lt;typename T, typename U&gt; void f(T, U) 更专业。标准中没有任何地方说不同类型对的扣除在偏序期间需要保持一致;指定的算法仅单独处理类型对。我的版本:对于函数调用具有参数的参数,使用来自A 的发明参数对使用来自P 的参数的合成模板声明的合成函数调用必须成功推导。

标签: c++ templates language-lawyer overload-resolution partial-ordering


【解决方案1】:

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

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

您的问题(和this one)促使我对事情的运作方式进行更多调查。我决定写这个答案不是因为我认为它具有权威性,而是为了整理我在一个地方找到的信息(它不适合 cmets)。希望对你有用。


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

上面从参数模板和 参数模板中的相应类型用作 PA如果特定的 P 不包含 模板参数 参与模板参数推导,即P不习惯 确定顺序。

我认为这不是一个好主意,原因如下:

  • 如果P是非依赖的,它根本不包含任何模板参数,所以它也不包含任何参与参数推导的参数,这将使粗体声明适用于它。但是,这会使template&lt;class T&gt; f(T, int)template&lt;class T, class U&gt; f(T, U) 无序,这是没有意义的。这可以说是对措辞的解释问题,但可能会引起混淆。
  • 它与 用于确定排序 的概念相混淆,这会影响 [14.8.2.4p11]。这使得template&lt;class T&gt; void f(T)template&lt;class T&gt; void f(typename A&lt;T&gt;::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 的第二个参数不用于偏序,所以从#1#2 的推导成功,反之则不行)。目前,这种说法是模棱两可的,可以说应该保持这种状态。


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

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

对于P / A 对:

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

注意事项:

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

将当前[p10]改为:

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

  • 对于用于确定排序的每一对类型,F 中的类型至少与 G 中的类型一样专业,并且,
  • 使用转换后的F作为参数模板,G作为参数模板进行推演时,推演完成后 对于所有类型对,类型中使用的所有模板参数来自 用于确定排序的G 具有值,而那些 所有类型对的值都是一致的。

F G 更专业 如果F 至少和F 一样专业 因为GG 至少不像F 那样专业。

将整个当前 [p11] 记为笔记。

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


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

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

一个没有的例子:

#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 拒绝了这个不明确的调用;不知道为什么。


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

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

这与Richard Corden's cmets 到his answer 一致,但我必须实际查看编译器代码才能理解所有含义(不是他的答案的错误,而是我自己的错误——程序员在代码中思考等等那个)。

我在this answer 中包含了有关 Clang 实现的更多信息。

【讨论】:

  • 优秀。一如既往。 +1
  • 这只是引起我注意的游戏,我赞成并奖励这个答案,但从未真正接受它。哎呀?无论如何,如果你在附近,我会收到another one
  • @jackX 该项目符号必须与下面修改后的 [p10] 中的第二个项目符号一起使用(G 中的类型中使用的所有参数都需要具有值)。换句话说,第一段中的项目符号为P/A 对分别指定条件,而[p10] 中的项目符号指定全局适用的条件,将模板G 作为一个整体。这是否回答你的问题?如果你想讨论你的例子,我认为打开一个聊天室并从这里链接到它是个好主意。
  • @jackX 我不同意“仅当”;在很多情况下,模板参数出现在推导的或推导的和非推导的上下文中,并且推导仍然失败。否则,是的,在您的示例中,以 #1 作为参数和 #2 作为参数的推论失败,因为在最后,T 仍然没有值。但是,在单个 P/A 对的级别上,第二对被认为是成功的,typename A&lt;T&gt;::aP。通常(不在此示例中),这允许从另一个 P/A 对潜在地推导出 T 并最终具有一个值。
  • @jackX 对于您的第一个问题,如果我正确理解了它的要点,我认为答案是“是”。我不会说“对于任何非演绎的上下文,它都可以演绎”,这有点自相矛盾;对于 P/A 对,即使出现在 P 中的某些参数可能仍然没有值,因为它们仅出现在该 P 内的非推导上下文中,推导被认为是成功的。这个明确规则的原因是措辞对每个单独的对使用这个成功/不成功的结果,这不同于“正常”的扣除([temp.deduct.type] 和朋友),其中......
【解决方案2】:

我认为关键在于以下陈述:

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

【讨论】:

  • 那么在 (6) 中是否完全跳过了参数中的非推导上下文?
  • @Barry:类型推导确定模板参数的替换,并进行一些检查(例如,类型通常必须匹配)。如果所有参数都有值,则将它们替换为类型以检查最终类型是否“有效”。非推导上下文对模板参数的值没有贡献,但在检查类型时会使用它们。问题是,我不确定您是否可以使用合成类型导致无效类型,即。 A&lt;T&gt;::type 有效但 A&lt;Q&gt;::type 无效。
  • 但是我们需要确定(6)中的类型匹配,这就是我们知道“b”推导失败的原因。如果在检查类型时仍然使用非推导的上下文,“a”怎么会不失败?你不必检查identity&lt;UniqueA&gt;::type
  • @Barry:[相同的评论 - 次要编辑] 推演失败,因为 T 先推演到 Q1,然后推演到 identity&lt;Q1&gt;::type。他们不被视为同一类型。反过来,T 推导出为 Q2,因为identity&lt;T&gt;::type 是非推导上下文,所以不会进行推导。我们只剩下T 的一个有效值,因此推演成功。
  • 类型推导不执行“转换”检查。 - 不是转换,而是兼容性 [14.8.2.5p1],这是一个比较模糊的概念,但据我所知可以大致理解为与下面详述的形式相同,即在[14.8. 2.5]。别忘了我们是在专门讨论 [14.8.2.4](部分排序期间的扣除),而不是任何其他部分。在这种情况下,该算法显然比仅类型推导成功进行更多检查,如您的项目符号 (6) 中所述。
猜你喜欢
  • 2013-06-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多