C++ Concept 重载决议探讨
测试环境
- OS: Ubuntu 22.04
 - CC: GCC 11.2.0
 
重载决议
重载决议(Overload Resolution)的定义,摘自 cppreference:
为了编译函数调用,编译器必须首先进行名字查找,对于函数可能涉及实参依赖查找,而对于函数模板可能后随模板实参推导。如果这些步骤产生了多个候选函数,那么需要进行重载决议选择将要实际调用的函数。
说人话就是,由于 C++ 支持函数重载、隐式类型转换、模板函数等特性,因此一个函数调用可能可以匹配上多个函数实现,编译器需要通过一些规则来选择一个最优实现参与编译。
重载决议规则比较复杂,但是可以总结几种常见的情况。
- 无论如何,最优先选择完全匹配的函数实现。
 - 
必须通过隐式转换匹配时,优先选择隐式转换少的函数实现,例如:
1 2 3 4 5 6 7 8 9 10
#include <iostream> void print(int a, char b) { std::cout << "int char" << std::endl; } void print(int a, int b) { std::cout << "int int" << std::endl; } int main() { // 选择 (int char) 的重载版本 print('a', 'b'); return 0; }
 - 
由于模板函数会生成完全匹配的函数实现,因此,如果没有模板特化,那么模板函数的优先级高于隐式转换:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
#include <iostream> void print(int a, char b) { std::cout << "int char" << std::endl; } void print(int a, int b) { std::cout << "int int" << std::endl; } template <typename T, typename U> void print(T a, U b) { std::cout << "template" << std::endl; } int main() { // 选择模板函数版本 print('a', 'b'); return 0; }
 
带有 Concept 的重载决议
重载决议失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 
#include <concepts>
#include <iostream>
template <typename T>
concept Addable = requires(T a) { a + a; };
template <std::integral T>
void print(const T &a) {
    std::cout << "integral" << std::endl;
}
template <Addable T>
void print(const T &a) {
    std::cout << "Addable" << std::endl;
}
int main() { print(12); }
 
 
上述代码在 GCC 11 中会编译失败,原因是 call of overloaded ‘print(int)’ is ambiguous。
代码中定义了一个新的概念 Addable,并且使用 Addable 概念与标准库概念 std::integral 分别实现了一个 print 的重载,对于 print(12) 而言,12 既满足 Addable 也满足 std::integral,这两种重载对于编译器而言是同等优先级的,因此编译器无法在其中择出一个最优实现。
想要解决这个问题也很简单,只需要引入 ! 来令两种重载互斥即可,例如在第二个重载中增加 !std::integral<T>:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
#include <concepts>
#include <iostream>
template <typename T>
concept Addable = requires(T a) { a + a; };
template <std::integral T>
void print(const T &a) {
    std::cout << "integral" << std::endl;
}
template <typename T>
  requires Addable<T> && (!std::integral<T>)
void print(const T &a) {
    std::cout << "Addable" << std::endl;
}
int main() {
    // 选择 std::integral 重载版本
    print(12);
}
 
 
由于第二个重载限定了 T 不能满足 std::integral,因此编译器为 print(12) 选择第一个重载。
合取引出的特异性
然而,Concept 之间并不总是同等优先级的。我们尝试引入一个新的概念 IntAddable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
#include <concepts>
#include <iostream>
template <typename T>
concept Addable = requires(T a) { a + a; };
template <typename T>
concept IntAddable = std::integral<T> && Addable<T>;
template <std::integral T>
void print(const T &a) {
    std::cout << "integral" << std::endl;
}
template <IntAddable T>
void print(const T &a) {
    std::cout << "IntAddable" << std::endl;
}
int main() {
    // 选择 IntAddable 重载版本
    print(12);
}
 
 
我们会发现,这回编译器不会报 ambiguous 了,并且选择了 IntAddable 的重载版本。我们从这里可以看出,由于 IntAddable 概念将 std::integral 概念包含在内,因此对于 print(12) 而言,IntAddable 比 std::integral 更加具有特异性,在编译器的眼中 IntAddable 的优先级就要高于 std::integral。这对于学习过 css 选择器的同学来说应该是非常熟悉的。
那么析取?
Concept 想要引入包含关系,除了合取语法之外还有析取语法。不过与合取相反的是,合取是 A && B 包含 A,但是析取是 A 包含 A || B。
下面的代码可以佐证这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 
#include <concepts>
#include <iostream>
template <typename T>
concept Addable = requires(T a) { a + a; };
template <typename T>
concept IntOrAddable = std::integral<T> || Addable<T>;
template <std::integral T>
void print(const T &a) {
    std::cout << "integral" << std::endl;
}
template <IntOrAddable T>
void print(const T &a) {
    std::cout << "IntOrAddable" << std::endl;
}
int main() {
    // 选择 std::integral 重载版本
    print(12);
}
 
 
结论
对于任意两个没有关联的概念 A 和 B,在重载决议时的优先级为:A && B > A == B > A || B。