深入理解 C++ 重载解析机制

在 C++ 中, 函数重载是一个非常强大的特性, 允许多个函数使用相同的名称, 但具有不同的参数类型. 重载解析决定了在给定的调用中, 编译器应选择哪个版本的重载函数. 本文将深入探讨 C++ 重载解析的工作原理, 帮助你在实际编程中更好地理解这一机制.

重载(Overload) vs 重写(Override)

多态(polymorphism)的定义

多态性是指一个实体能够表现为多种形式, 并存在两种或两种以上的可能性. 这种特性使得对象能够根据上下文以不同的方式运行.

  1. 编译时多态性: 编译时多态性主要通过函数, 方法或构造函数的重载实现. 这种重载机制允许程序根据数据类型选择合适的函数调用, 从而实现灵活性和代码的可重用性.
  2. 运行时多态性: 运行时多态性通过方法重写(Override)实现. 程序在运行时根据对象的实际类型调用正确的方法, 从而支持动态行为.

什么是函数重载?

重载是一种允许为具有不同签名(Signature)的多个声明分配相同名称的机制. 常见的有函数重载和运算符重载.

运算符重载

运算符重载允许为特定运算符定义多种操作. 运算符+ 可以用于添加两个数字或连接两个字符串. 这种重载通过重载解析过程得以实现.

int a = 1, b = 2;
std::string sa = "1", sb = "2";

int c = a + b;             // 3
std::string sc = sa + sb;  // "12"

什么是方法重写

为什么需要重载解析

当一个函数调用在编译时包含多个候选函数时, 编译器需要通过重载解析机制来选择最匹配的版本. 重载解析不仅考虑函数的名称, 还需要匹配传递给函数的参数类型, 以确保调用的正确性. 这种机制避免了使用冗长的函数名称, 例如 funStr()funInt(), 从而提高了代码的可读性和简洁性.

fun("mountain");
fun(17);

重载声明(Declaring Overloads)

重载适用于自由函数, 类(class)的方法和构造函数. 当这些方法满足如下条件时被称为重载:

需要注意的是声明的顺序没有影响.

代码样例

// 重载函数 1
void fun(int) {}

// 重载函数 2
void fun(int, double) {}

int main() {
  fun(42);        // 调用第一个重载
  fun(42, 3.14);  // 调用第二个重载
}

什么是重载解析(Overload Resolution)

重载解析是选择最合适重载的过程. 编译器在编译时决定调用哪个重载,

模板函数或方法参与重载解析过程, 如果两个重载被视为相等, 则非模板函数将始终优先于模板函数.

哪些情况不能重载

  1. 两个函数仅在返回类型上有所不同. 由于使用返回值是可选的, 因此编译器将其视为定义同一函数两次.

    int add(int a, int b) {}
    double add(int a, int b) {}
  2. 两个函数仅在其默认参数上有所不同. 默认值不会使函数签名不同.

    void fun(int a, int b = 10);
    void fun(int a, int b = 5);
    void fun(int a = 1, int b = 10);
  3. 两个具有相同签名的方法, 其中一个被标记为"静态(static)"

    void fun() {}
    static void fun() {}

重载解析过程概述

重载解析由编译器计算, 在通常情况下, 此过程会导致调用预期的重载. 但是如果存在如下的情况就会变得很复杂, 并且如果编译器选择了错误的重载函数, 这对用户来说就比较难觉察到这一点.

何时使用重载而不是使用模板

重载解析开始前

重载解析细节

什么因素导致候选函数不可行或无效

查找最佳重载的过程

  1. 创建候选列表
  2. 删除无效重载
  3. 对剩余候选进行排序
  4. 如果候选列表中恰好有一个函数的排名高于所有其他函数, 则它将赢得重载解析过程
  5. 如果最高排名并列, 则使用决胜局
#include <iostream>
#include <string>

// candidate A
int lookUp(const std::string* key) { return 'A'; }

// candidate B
int lookUp(std::string* key) { return 'B'; }

int main() {
  std::string* str = new std::string("text");
  int value = lookUp(str);
  std::cout << value << std::endl; // 调用哪个?
}

答案:

str的类型为std::string*, 所以选择B

candidate B

测试题

#include <iostream>
// overload A1
void fun(char value) { std::cout << "A1" << std::endl; }

// overload A2
void fun(long value) { std::cout << "A2" << std::endl; }

int main() {
  fun(42);  // 调用哪个?
}

答案:

ambiguous (编译失败)

选择候选函数

决胜局是重载解析的最后一步, 用于确定哪个候选函数更匹配

C++20 新增的决胜局

测试题

#include <iostream>

// overload B1
void fun(char value) { std::cout << "B1" << std::endl; }

// overload B2
template <typename T>
void fun(T value) {
  std::cout << "B2" << std::endl;
}

int main() {
  fun(42);  // 调用哪个?
}

答案

B2

当候选集没有最佳匹配时

如何解决模糊函数调用

当没有最佳匹配时, 编译会生成错误消息 - “没有匹配的函数可供调用”, 并列出可能的候选者, 即使没有可行的候选者

void fun(char value) {}

int main() { fun('x', nullptr); }

当最佳匹配不是您想要的

做一些测试

下面的测试有些存在ambiguous的情况.

Question 1

对比着看下面的两个例子. 思考一下答案.

#include <iostream>

// overload C1
void fun(double, int, int) { std::cout << "C1" << std::endl; }

// overload C2
void fun(int, double, double) { std::cout << "C2" << std::endl; }

int main() {
  fun(4, 5, 6);  // 调用哪个?
}
#include <iostream>
// overload D1
void fun(int, int, double) { std::cout << "D1" << std::endl; }

// overload D2
void fun(int, double, double) { std::cout << "D2" << std::endl; }

int main() {
  fun(4, 5, 6);  // 调用哪个?
}

答案:

C1/C2 ambiguous(编译错误)
D1

为什么会是这种情况? 从参数匹配上来讲, 1的情况是有两个参数类型 int 匹配, 2只有一个参数类型匹配, 似乎是应该选C1D1, 但是实际情况则并非如此. 在对候选项进行排序的时候, 编译器会这样做

Question 3

#include <iostream>
// overload E1
void fun(int &) { std::cout << "E1" << std::endl; }

// overload E2
void fun(int) { std::cout << "E2" << std::endl; }

int main() {
  int x = 42;
  fun(x);  // 调用哪个?
}

答案:

ambiguous(编译错误)

Question 4

#include <iostream>
// overload F1
void fun(int &) { std::cout << "F1" << std::endl; }

// overload F2
void fun(int) { std::cout << "F2" << std::endl; }

int main() {
  fun(42);  // 调用哪个?
}

答案:

F2

Question 5

#include <iostream>
// overload G1
void fun(int &) { std::cout << "G1" << std::endl; }

// overload G2
void fun(int &&) { std::cout << "G2" << std::endl; }

int main() {
  int x = 42;
  fun(x);  // 调用哪个?
}

答案:

G1

Question 6

#include <iostream>
// overload H1
void fun(int &) { std::cout << "H1" << std::endl; }

// overload H2
void fun(int &&) { std::cout << "H2" << std::endl; }

int main() {
  fun(42);  // 调用哪个?
}

答案:

H2

总结

重载解析是 C++ 中一个非常强大但复杂的特性. 理解重载解析的细节, 尤其是在处理模棱两可的错误时, 将帮助你写出更高效, 可维护的代码. 在实际编程中, 尽量避免模板和非模板函数的重载冲突, 使用显式转换来消除模糊匹配的情况.

参考链接