使用默认函数模板参数的意外重载解析

Unexpected overload resolution with default function template parameter

本文关键字:意外 重载 参数 默认 函数模板      更新时间:2023-10-16

我正在经历一个似乎非常意外的重载解析行为。下面的代码会被gcc和clang拒绝,并出现歧义错误:

template <typename T>
struct A
{
    typedef T key_type;
};
template <typename T>
void foo(A<T> rng, T val);
template <typename T, typename U = T>
void foo(T, typename U::key_type);
int main()
{
    A<int> i;
    foo(i, 0);
}

错误是:

test.cpp:16:5: error: call to 'foo' is ambiguous
    foo(i, 0);
    ^~~
test.cpp:8:6: note: candidate function [with T = int]
void foo(A<T> rng, T val);
     ^
test.cpp:11:6: note: candidate function [with T = A<int>, U = A<int>]
void foo(T, typename U::key_type);
     ^

我希望两者都是精确匹配的,但第一个重载在部分排序中胜出,因为在第一个参数中A<T>T更专门化。

令我惊讶的是,如果我将第二个签名更改为:

template <typename T, typename U = T>
void foo(T, typename T::key_type);

gcc和clang现在都接受代码,并选择我最初期望的第一个重载。

我看不出这个改变会对行为产生什么影响:我所做的只是用默认值(T)代替既没有明确指定也没有推导的模板参数(U)的使用。

然后,改变之前的行为是意想不到的,所以也许我错过了什么。

谁能解释一下:

  1. 为什么第一种情况是模棱两可的;和
  2. 为什么我所做的更改解决了歧义?

如果它是相关的,我测试的编译器版本是gcc 4.8.0和clang的最新主干构建。

问题是,在实参推导之后,是否存在将推导出来的模板实参替换到形参列表中的阶段。在此阶段,默认实参将用于尚未推导出的模板形参。

这个额外的步骤是在什么情况下执行的,而不是在什么情况下执行的问题是一个活跃的核心问题,http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_active.html#697。

如果您执行了额外的替换步骤,您还需要实例化模板(否则替换步骤本身就没有多大意义)。你也可以只选择默认参数,而不做替换,但在标准中这两件事是一起发生的,所以作为一个实现者,我不会选择那条路。

部分排序在很大程度上独立于对其进行部分排序的上下文(考虑一些上下文相关的事情-例如,没有显式调用参数的函数参数被忽略)。它也独立于模板形参是否有显式的模板实参传递(所以如果你给U一个值,部分排序将不会"记住"它。

Clang和GCC不执行替换步骤,也不使用模板默认参数。因此,当将TU::key_type进行比较以找出U时,他们会说:"嗯,一个非推断的背景。"我们会说"成功,没有错!"'用于此参数'。在比较TT::key_type时也会发生同样的情况。当它沿着另一个方向比较WhatEver::key_typeT时,T也可以推断出依赖类型。所以对于第二个参数,在你的两次尝试中,两个模板至少是彼此一样专门化的。

然而,重要的区别在于,在演绎之后,参数类型列表中使用的所有参数都必须有值。

在大多数情况下,所有模板形参都必须有值才能成功推导,但对于部分排序目的,只要不是在用于部分排序的类型中使用,模板形参可以不带值。[注:在非推导的上下文中使用的模板参数被认为是使用的。]

在你的第二次尝试中,T是由第一个参数推导出来的,所以在比较参数/实参类型之后没有发生什么不好的事情。在第一次尝试中,没有推导出U,因此第一个模板没有被认为比第二个模板"更专业"。