Logo
Section 1 / 7

概览

在本节课中,我们将概述 C++20 所需掌握的核心概念。

导入

概览

在本节课中,我们将概述 C++20 所需掌握的核心概念。

在 C++20 之前,我们有两种截然不同的方式来思考函数或用户自定义类型(类)。函数或类可以针对特定类型定义,也可以针对泛型类型定义。在后一种情况下,我们称之为函数模板或类模板。

在讨论 C++ 中的函数时,存在两个极端。让我们仔细分析两种函数实现:

过于具体 vs 过于泛化

### 过于具体

1#include <iostream> 2 3void needInt(int i){ // 1 4 std::cout << i << std::endl; 5} 6 7int main(){ 8 double d{1.234}; 9 needInt(d); 10}

### 过于泛化 ```c++ #include

template T gcd(T a, T b){ if (b == 0){ return a; } else { return gcd(b, a % b); } }

int main(){ std::cout << gcd(100, 10) << std::endl; std::cout << gcd(3.5, 4.0) << std::endl; }

1</div> 2</div> 3 4## 过于具体 5 6为每种具体类型都定义一个函数或类是一项繁琐的工作。为了避免这种负担,类型转换常常被用来缓解问题,但这也带来了新的隐患。 7 8### 截断转换 9 10我们有一个函数 `needInt(int a)`(第 3 行),它可以接受一个 `double` 类型的参数。此时会发生截断转换。 11 12```c++ 13#include <iostream> 14 15void needInt(int i){ 16 std::cout << i << std::endl; 17} 18 19int main(){ 20 double d{1.234}; 21 needInt(d); 22}

我们假设这并不是我们期望的行为。我们从一个 double 开始,最终却得到了一个 int。

但转换也可以反向进行。

整数提升

我们定义了一个用户自定义类型 MyHouse。MyHouse 的实例可以通过两种方式构造:当在第 5 行不带参数调用时,其成员 family 被设置为空字符串,表示房子还是空的。为了快速判断房子是否为空,我们在第 8 行实现了 bool 类型的转换运算符。

1#include <iostream> 2#include <string> 3 4struct MyHouse{ 5 MyHouse() = default; 6 MyHouse(const std::string& fam): family(fam) {} 7 8 operator bool() { return !family.empty(); } 9 10 std::string family = ""; 11}; 12 13void needInt(int i){ 14 std::cout << "int: " << i << std::endl; 15} 16 17int main(){ 18 std::cout << std::boolalpha << std::endl; 19 20 MyHouse firstHouse; 21 if (!firstHouse){ 22 std::cout << "The firstHouse is still empty." << std::endl; 23 } 24 25 MyHouse secondHouse("grimm"); 26 if (secondHouse){ 27 std::cout << "Family grimm lives in secondHouse." << std::endl; 28 } 29 30 std::cout << std::endl; 31 32 needInt(firstHouse); 33 needInt(secondHouse); 34 35 std::cout << std::endl; 36}

现在,当需要 int 类型时,MyHouse 的实例也可以被使用。这很奇怪!

由于第 8 行的 bool 重载运算符,MyHouse 的实例可以被当作 int 使用,因此也能参与算术表达式:auto res = MyHouse() + 5。这并非我们的本意!不过这里仅作完整说明。从 C++11 开始,我们应该将这类转换运算符声明为 explicit,以避免隐式转换的发生。

过于泛化

泛型函数或类可以接受任意值作为参数。如果传入的值不满足函数或类的要求,编译器会在编译时报告错误。

1#include <iostream> 2 3template<typename T> 4T gcd(T a, T b){ 5 if (b == 0) return a; 6 else return gcd(b, a % b); 7} 8 9int main(){ 10 std::cout << std::endl; 11 12 std::cout << gcd(100, 10) << std::endl; 13 14 std::cout << gcd(3.5, 4.0) << std::endl; 15 std::cout << gcd("100", "10") << std::endl; 16 17 std::cout << std::endl; 18}

这个错误信息的问题在哪里?

当然,它非常长且难以理解,但我们真正关心的是另一个问题。编译失败的原因是 double 和 C 字符串都不支持 % 操作符。这意味着错误源于对 double 和字符串类型的模板实例化失败。这种情况发生得太晚了,因此非常糟糕。对于 double 或 C 字符串类型,根本就不应该允许进行模板实例化。对参数的要求应当成为函数声明的一部分,而不是模板实例化失败的副作用。

这时,概念(concepts)便能为我们解困。

概念拯救我们

借助概念,我们可以获得一种中间状态。我们可以定义作用于语义类别上的函数或类。也就是说,函数或类的参数既不是过于具体,也不是过于泛化,而是具有明确名称的一组要求,例如 Integral。

优势

  • 将模板参数的要求作为接口的一部分进行表达
  • 支持函数重载和类模板特化
  • 通过比较模板参数的要求与实际传入的参数,生成大幅改进的错误信息
  • 可作为泛型编程中的占位符使用
  • 允许你自定义自己的概念
  • 适用于各种类型的模板

在下一节中,我们将了解概念的历史,并探讨概念的未来。

设计

历史