【问题标题】:How c++ deduce the right overloaded function?c++如何推导出正确的重载函数?
【发布时间】:2022-01-11 14:57:57
【问题描述】:

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

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:也许在这里看看 deducing 是如何工作的不是一个好主意,因为我可能永远不知道 pow 是如何真正实现的。还是这样?

【问题讨论】:

  • 用户定义函数的行为是一样的,你可以试试看。比如模棱两可的时候:godbolt.org/z/GKEdfbs83
  • overload_resolution 不是一个简单的主题,当我们进入细节时。
  • @mch 奥基,我明白为什么在你的例子中在这里模棱两可。所以我决定做同样的事情,但以我的例子为例。我得到了编译错误:godbolt.org/z/Tz7xhqxce

标签: c++ c++98


【解决方案1】:

编译器将使用名称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,但它对中间结果使用整数运算。

【讨论】:

  • 即使我们删除了using namespace std;,该程序仍然有效,如here所示。你能解释一下为什么给定here 的程序在OP 给出的程序(在他/她的原始问题中)有效时不起作用。两个程序都有相同的重载。
  • @AanchalSharma 为什么 pow 在没有 std 的情况下也能工作? O.o 我认为这是一些 ADL 的东西,但后来意识到如果没有正确的参数,我无论如何都无法查看正确的命名空间。或者可能在调用 operator&lt;&lt; ADL 时涉及这里? upd:没有cout &lt;&lt; 也可以工作,因此可能根本没有 ADL?
  • @Learpcs:向后兼容 C。ADL 取决于参数,int 没有关联的命名空间
【解决方案2】:

tl;博士:

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

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

  • 在您的 Godbolt 示例中,您实际上调用了 std::pow&lt;int, int&gt;,即它的模板版本。这是因为您使用的是一个相当新的 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 的示例
  • 另一个问题是 &lt;cmath&gt; 本身 - 因为它是 c 中 &lt;math.h&gt; 的 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 standard13.3 Overload resolution 部分就是您要查找的内容。

现在进入它:

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

    13.3.2 可行函数

    (1) 首先,作为一个可行的函数,一个候选函数应该有足够的参数在数量上与 列表中的参数。

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

    13.3.2 可行函数

    (3) 其次,为了使 F 成为一个可行的函数,每个参数都应该存在一个隐式转换序列 (13.3.3.1) 将该参数转换为 F 的相应参数。

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

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

    • Exact Match 是最好的,这适用于类型完全匹配或只是将数组衰减为指针或函数衰减为函数指针的情况。
    • Promotion 稍微差一点,这适用于例如从一种整数类型转换为更大的类型或从浮点类型转换为更大的类型(但不是在整数和浮点之间,不是到更小的类型)李>
    • Conversion 是最糟糕的一个,它涵盖了 int 和 float 类型的“降级”(例如 int -&gt; chardouble -&gt; float)以及整数和浮点类型之间的转换,例如int -&gt; 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 名已经被淘汰了,因为他们的分数更差。
    这留下了最后 2 个,但实际上那些得分相同,所以这是平局!
    -> 你会得到一个模棱两可的函数调用错误

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

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

³ 极其简化的版本。这不是实际编译器会这样做的方式。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-06
    • 2012-01-24
    • 2016-01-31
    相关资源
    最近更新 更多