C++ 如何推导出正确的重载函数?

How c++ deduce the right overloaded function?

在 C++98 中 pow 有 5 个不同的版本:

double pow (double base, double exponent);
float pow (float base, float exponent);
long double pow (long double base, long double exponent);
double pow (double base, int exponent);
long double pow (long double base, int exponent);

我的老师告诉我,在 c++11 之前(在添加模板版本的 pow 之前),可能会出现 c++ 无法推断选择哪个重载的错误。这对我来说似乎是合理的。例如考虑这个代码:

#include <iostream>
#include <cmath>
using namespace std;

int main()
{
    cout << pow(4, 3);
    cin.get();
}

我正在用两个整数参数调用 pow() 函数。我没有 pow() 这样的重载,所以也许我应该做一些隐式转换?好的,但是选择哪一个呢?我无法选择如何转换,因为我在选择重载时有歧义。但是我试图编译这段代码(在 std=c++98 模式下)并且它 worked。为什么?

好的,可能是因为第二个参数是整数。因此我只能在 double pow (double base, int exponent)long double pow (long double base, int exponent) 之间做出选择。但是仍然决定选择哪一个编译器?如果我调用 pow(4, 3ll) 怎么办?它仍然可以编译,但类型推导对我来说不太明显。

upd:也许在这里看到推导是如何工作的是个坏主意,因为我可能永远不知道 pow 是如何真正实现的。或者是?

编译器将使用名称pow来寻找一组候选函数。因为你写了 using namespace std; (bad idea) 这包括 std::pow 重载。有 [edit]至少[/edit] 5 个这样的重载。 (编译器可能会添加更多重载以提高效率)。

接下来,对于每个可以接受 2 个参数的重载,编译器将确定 2 个转换序列。因此,编译器总共以(至少)5x2 转换序列结束

在重载决策中,选择具有最佳转换序列的重载。这要求在所有重载中有一个总赢家。如果没有明确的赢家,那将是模棱两可的。对于 5 个重载和 2 个转换序列,这很容易发生:一个重载可以为第一个序列提供最佳转换序列,另一个重载为第二个序列。

请注意,在这种情况下,您希望转换序列之间存在联系。例如,

double pow (double base, double exponent);
double pow (double base, int exponent);

的第一个参数将具有相同的转换顺序(在您的情况下,从 intdouble。关系不是大问题,因为它们只会影响两个转换中的一个序列。

转换序列的具体规则有点复杂,但主要规则是:不转换最好,从浮点类型到整数类型的转换不好,反之亦然,用户自定义转换最差类型的转换,如果您需要按顺序进行多种类型的转换,那就更糟了。

对于此示例中的 pow(3,4),不需要用户定义的转换,因此两个决定因素是您是否需要任何转换,如果需要,是否是从 int 到的转换浮点类型。

额外 重载的基本原理是 pow(x,y) 的某些实现可以使用 pow(x,y)==pow(x*x,y/2) 进行优化。当然,如果 y 是整数,那么优化效果最好。特别是,这允许编译器将 pow(3,4) 计算为 pow((3*3)*(3*3), 1) 。这仍然会产生 81.0 但它对中间结果使用整数运算。

tl;博士:

  • std::pow 是一个困难的例子,因为编译器提供了更多的重载并且 c 版本被认为是 gcc 的内置,所以你的问题很难用它来证明。
  • 是的,如果您列出的 5 个函数是唯一参与重载解析的函数,那么调用实际上是不明确的。

pow() 是一个相当棘手的函数,因为有一些陷阱:

  • 在你的 godbolt 示例中,你实际调用了 std::pow<int, int>,即它的模板化版本。这是因为您使用的是一个相当新的 gcc 版本,它提供了 std::pow 的模板化重载以获得更好的性能。

    您可以在生成的程序集第 6 行中很好地看到这一点:(看最后)

    call    __gnu_cxx::__promote_2<int, int, __gnu_cxx::__promote<int, std::__is_integer<int>::__value>::__type, __gnu_cxx::__promote<int, std::__is_integer<int>::__value>::__type>::__type std::pow<int, int>(int, int)
    

    因此,为了对此进行测试,您可以做一些事情:

    • 切换到还没有模板化重载的旧版本 gcc,即 gcc 4.1.2:
      godbolt example
    • 或使用用户指定的函数对其进行测试,例如来自@mch 的示例:
      godbolt example
  • 另一个问题是 <cmath> 本身 - 因为它是 c 中 <math.h> 的 c++ 等价物,gcc 和 clang 也暴露了 c pow function (没有 std:: 前缀)-这就是为什么即使删除 using namespace std;.
    您的代码也能正常工作的原因 由于 c 不允许函数重载,因此只有一个版本带有 double。 (浮动版本是powf,长双一powl)。
    另外 gcc 将它们视为 be built-ins,因此在这种情况下它可以完全摆脱 pow 调用,因为它使用常量作为参数,即使在 -O0.
    这是@Aanchal Sharma 提供的示例中发生的情况:godbolt example

    mov     rax, QWORD PTR .LC0[rip]
    movq    xmm0, rax
    // calculated value (64 as a double)
    .LC0:
          .long   0
          .long   1078984704
    

    所以 gcc 只是在编译时计算结果并将其内联。 (请注意,这里根本没有重载解析问题,因为 c 中只有一个 pow 函数 - 而 c++ 函数不可用,因为 using namespace std; 未在其中注释)

    您可以通过将 -fno-builtin 作为编译器参数传递来抑制这种情况,在这种情况下,gcc 将尽职尽责地发出对 double pow(double, double) 的调用:godbolt example

    movsd   xmm0, QWORD PTR .LC0[rip]
    mov     rax, QWORD PTR .LC1[rip]
    movapd  xmm1, xmm0
    movq    xmm0, rax
    call    pow
    
    .LC0: // 3 as a double
       .long   0
       .long   1074266112
    .LC1: // 4 as a double
       .long   0
       .long   1074790400
    

所以回答你的问题:

1。为什么有效?

因为 gcc 实际上提供了 std::pow 的模板化重载,在这种情况下是完美的匹配:

template<typename _Tp, typename _Up>
inline typename __gnu_cxx::__promote_2<_Tp, _Up>::__type pow(_Tp __x, _Up __y);

2。假设没有模板重载,编译器将如何决定使用哪个重载?

确定最佳重载的规则相当复杂,所以我会稍微简化它们(我将完全忽略用户定义的转换函数、模板函数、可变参数函数和其他一些与问题)

如果您想更全面地了解实际规则,可以查看 c++98 standard,第 13.3 Overload resolution 部分就是您要查找的内容。

现在开始:

  • 首先,所有参数数量不正确的函数都被剔除。即,如果有一个 pow 函数接受三个参数(并且第三个参数没有默认值)——它将被直接消除。

    13.3.2 可行函数

    (1) First, to be a viable function, a candidate function shall have enough parameters to agree in number with the arguments in the list.

  • 其次,您提供的参数实际上需要转换为相关重载的参数。如果无法在它们之间进行转换,则会消除过载。

    13.3.2 可行函数

    (3) Second, for F to be a viable function, there shall exist for each argument an implicit conversion sequence (13.3.3.1) that converts that argument to the corresponding parameter of F.

  • 之后,编译器留下了一组重载 - 标准称它们为 可行函数 - 可以在给定的上下文中调用。现在编译器需要实际决定哪一个是最好的,即 最佳可行函数 ,应该最终被调用。

  • 现在我们进入有趣的部分 - 根据它们的适合程度对过载进行评级。参数转换基本上可以具有三个主要类别:
    Exact Match > Promotion > Conversion ¹

    • Exact Match 是最好的,如果类型完全匹配或者只是将数组衰减为指针或将函数衰减为函数指针,则适用。
    • Promotion 稍微差一点,这适用于您,例如从一种整数类型转换为更大的整数类型或从浮点类型转换为更大的整数类型(但整数和浮点之间 not,并且 not更小的类型)
    • Conversion 是最差的一个,涵盖了 int 和 float 类型的“降级”(例如 int -> chardouble -> float)以及整数和浮点类型之间的转换,例如int -> double.

    编译器不关心确切的转换是什么,只关心它属于三个类别中的哪一个。 ²

    所以你可以这样想:³

    • 每个函数根据参数所需的转换得到一个分数
    • Exact Match 值 0 分
    • Promotion 值 -1 分
    • Conversion值得-2分
    • 调用得分最高的函数
    • 平局,叫号不明确
  • 所以现在要对 std::pow 重载进行实际排名:

    // when called as pow(1, 1)
    
    // 2x Conversion -> score -4
    double pow (double base, double exponent);
    
    // 2x Conversion -> score -4
    float pow (float base, float exponent);
    
    // 2x Conversion -> score -4
    long double pow (long double base, long double exponent);
    
    // 1x Conversion, 1x Exact -> score -2
    double pow (double base, int exponent);
    
    // 1x Conversion, 1x Exact -> score -2
    long double pow (long double base, int exponent);
    

    因此前 3 个已经被淘汰 table,因为他们的分数更差。
    这还剩最后2个,但实际上分数相同,所以是平局!
    -> 你会得到一个不明确的函数调用错误

脚注:
¹ 用户定义的转换函数、可变参数函数和模板还有一些规则
(参见 13.3.3.1 Implicit conversion sequences

² 其实还有很多事情需要考虑
(完整列表请参阅 13.3.3.2 Ranking implicit conversion sequences

³ 极其简化的版本。这不是实际编译器的处理方式。