首页 文章

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

提问于
浏览
19

考虑以下简单(在模板问题的范围内)示例:

#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<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”重载?

2 回答

  • 22

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

    为了使事情变得更有趣,MSVC(我测试了12和14)拒绝这个调用是模棱两可的 . 我认为标准中没有任何东西能够最终证明哪个编译器是正确的,但我想我可能已经知道差异来自何处;下面有关于此的说明 .

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


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

    上面从参数模板中提名的每种类型和参数模板中的相应类型都用作P和A的类型 . 如果特定P不包含参与模板参数推导的模板参数,则不使用该P来确定订购 .

    我认为这不是一个好主意,原因有以下几点:

    • 如果 P 是非依赖的,它也不包含参与参数推导的任何参数,这将使粗体语句适用于它 . 但是,这会使 template<class T> f(T, int)template<class T, class U> f(T, U) 无序,这没有意义 . 这可以说是对措辞的解释,但可能引起混淆 .

    • 它与用于确定排序的概念混淆,影响[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 的第二个参数不用于部分排序,因此扣除成功从 #1#2 ,但不是相反) . 目前,这个电话是模棱两可的,应该可以说是这样 .


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

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

    对于P / A对:如果P是非依赖性的,当且仅当P和A是相同类型时,推论被认为是成功的 . 将推断的模板参数替换为P中出现的未推导的上下文不会被执行,并且不会影响推论过程的结果 . 如果成功推导出P的所有模板参数的模板参数值,除了仅出现在非推导的上下文中的那些参数值,则扣除被认为是成功的(即使在P中使用的某些参数在扣除过程结束时保持没有值)特定的P / A对) .

    笔记:

    • 关于第二个要点:[14.8.2.5p1]讨论了在替换推导出的值(称之为推导出的 A )之后找到将产生 P 的模板参数值,与 A 兼容 . 这可能会导致对部分订购期间实际发生的事情产生混淆;没有替代品 .

    • 在某些情况下,MSVC似乎没有实现第三个要点 . 有关详细信息,请参阅下一节 .

    • 第二和第三个要点还旨在涵盖 P 具有 A<T, typename U::b> 等形式的情况,这些情况不在第1391期的措辞中 .

    将当前[p10]更改为:

    函数模板F至少与函数模板G一样专用,当且仅当:对于用于确定排序的每对类型,F中的类型至少与G中的类型一样专用,并且,当使用G进行演绎时将变换后的F作为参数模板,将G作为参数模板,在对所有类型对进行推导后,用于确定排序的G类型中使用的所有模板参数都具有值,并且这些值在所有类型中都是一致的对类型 . 如果F至少与G一样专用,并且G至少不像F那样专用,则F比G更专业 .

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

    (由1391至[14.8.2.5p4]的决议增加的说明也需要调整 - 对于[14.8.2.1]是好的,但对[14.8.2.4]则没有 . )


    对于MSVC,在某些情况下,看起来 P 中的所有模板参数都需要在扣除特定的 P / A 对期间接收值,以便从 AP 成功扣减 . 我认为这可能是导致你的例子和其他人实现分歧的原因,但我似乎也没有用,所以我不确定该相信什么 .

    上面的语句确实似乎适用的另一个示例:在示例中将 template<typename T> void bar(T, T) 更改为 template<typename T, typename U> 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的模板参数推导算法只是跳过它,通过返回特定步骤的"success" . 这不仅发生在部分排序期间,而且发生在所有类型的推论中,而不仅仅发生在函数参数列表的顶层,而是以复合类型的形式遇到非推断的上下文时递归发生 . 出于某种原因,我第一次看到它时发现这令人惊讶 . 考虑到它,它确实有意义,并且符合标准([...]不参与[14.8.2.5p4]中的类型推导[...]) .

    这与his answerRichard Corden's注释一致,但我必须实际看到编译器代码才能理解所有含义(不是他的答案的错,而是我自己的 - 程序员在代码中思考所有这些) .

    我在this answer中执行了've included some more information about Clang' .

  • 5

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

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

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

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

    • 替换显式参数,并检查结果函数类型是否有效 . (14.8.2 / 2)

    • 执行类型推导,并替换生成的推导参数 . 结果类型必须再次有效 . (14.8.2 / 5)

    • 功能模板步骤1和2中的成功是专门的,并包含在重载决策的重载集中 . (14.8.3 / 1)

    • 转换序列通过重载分辨率进行比较 . (13.3.3)

    • 如果两个函数特化的转换序列不是'better',则使用部分排序算法来查找更专业的函数模板 . (13.3.3)

    • 部分排序算法仅检查类型推导是否成功 . (14.5.6.2/2)

    编译器已经在步骤4中知道在使用实参数时可以调用两个特化 . 步骤5和6用于确定哪些功能更专业 .

相关问题