avatar Nihil

Nichts Hsu

  • 首页
  • 子域
  • 分类
  • 标签
  • 归档
  • 关于
首页 C++ Concept 重载决议探讨
文章

C++ Concept 重载决议探讨

发表于 2022/12/16 更新于 2022/12/16
作者 Nichts Hsu
6 分钟阅读
C++ Concept 重载决议探讨
C++ Concept 重载决议探讨

测试环境

  • OS: Ubuntu 22.04
  • CC: GCC 11.2.0

重载决议

重载决议(Overload Resolution)的定义,摘自 cppreference:

为了编译函数调用,编译器必须首先进行名字查找,对于函数可能涉及实参依赖查找,而对于函数模板可能后随模板实参推导。如果这些步骤产生了多个候选函数,那么需要进行重载决议选择将要实际调用的函数。

说人话就是,由于 C++ 支持函数重载、隐式类型转换、模板函数等特性,因此一个函数调用可能可以匹配上多个函数实现,编译器需要通过一些规则来选择一个最优实现参与编译。

重载决议规则比较复杂,但是可以总结几种常见的情况。

  1. 无论如何,最优先选择完全匹配的函数实现。
  2. 必须通过隐式转换匹配时,优先选择隐式转换少的函数实现,例如:

    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;
    }
    
  3. 由于模板函数会生成完全匹配的函数实现,因此,如果没有模板特化,那么模板函数的优先级高于隐式转换:

    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。

杂记, Cpp
c++ 编程语言 c++20
本文由作者按照 CC BY 4.0 进行授权
分享

最近更新

  • 『I Wanna』 Best Bye To 2016
  • [译] Rust 中的内联
  • [Rust] 幽灵索引类型与匿名结构体
  • [C++] 深入了解左值与右值
  • Android.bp 中启用 openmp
外部链接
  • 996.icu
  •  此博客的 Github 仓库
  •  Olimi 的个人博客

文章内容

相关文章

2023/04/24

初探 C++20 Coroutine

前言 近段时间研究了一下 C++20 的协程(Coroutine),大概了解了其中的工作原理,做一下记录。 初次接触 Coroutine 时,给我的感觉是一脸懵逼的。和其他语言简单的 async、await 不同,想要使用 C++20 的 Coroutine,它要求你定义一个包含 promise_type 的类型,其中 promise_type 又需要至少包含 get_return_ob...

2022/11/23

C++20 Concept

模板 在为静态类型语言开发代码时,我们很经常遇到这样的情况:我们需要为多个数据类型实现相同的功能。放在 C 语言中,我们不得不为他们各自定义一个函数或结构体,例如: int add(int x, int y); unsigned addu(unsigned x, unsigned y); float addf(float x, float y); // ... struct Vecto...

2023/05/11

C++ Coroutine VS Rust Async

在 C++20 中,我们有 Coroutine,在 Rust 中,我们有 Async。严格来说,二者之间没有完全等效的概念,但是我们可以找到一些相似之处,进而了解 C++ Coroutine 与 Rust Async 设计上的异同点。 由于我的上一篇文章介绍了 C++ 的 Coroutine,因此我们本文主要以 C++ Coroutine 的视角来看 Rust 的 Async。 为了更好...

Type-C 接口 CC 针脚的工作模式

Rust 2022 年稳定的语法

© 2024 Nichts Hsu. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

热门标签

编程语言 教程 rust c++ android c++20 usb 翻译 linux qt

发现新版本的内容。