第九章 C++20相关问题

# 第九章 C++20相关问题

接下来的几个问题是关于C++20中最新C++特性的一些基础知识。知道这些问题的答案,至少证明你在努力跟上这些变化。

# 问题64:C++中的概念(Concepts)是什么?

概念是对模板的一种扩展。它们是编译时断言,可以用来表达泛型算法对其模板参数的期望。

概念让你能够正式记录对模板的约束,并让编译器来强制执行这些约束。另外,你还可以利用这种强制检查,通过基于概念的重载来提高程序的编译速度。

概念的主要用途有:

  • 为模板编程引入类型检查。
  • 简化模板实例化失败时的编译器诊断信息。
  • 根据类型属性选择函数模板重载和类模板特化。
  • 约束自动类型推导。

下面是定义概念的方式:

template<typename  T>
concept  integral  = std::is_integral<T>::value;
1
2

然后可以这样使用:

auto  add(integral auto  a, integral auto  b) {
    return  a+b;
}
1
2
3

概念有哪些优点呢?

  • 模板的要求成为接口的一部分。
  • 函数的重载或类模板的特化可以基于概念。
  • 我们能得到更好的错误信息,因为编译器会将模板参数的要求与实际的模板参数进行比较。
  • 你可以使用预定义的概念,也可以定义自己的概念。
  • auto和概念的使用是统一的。你可以使用conceptName auto语法,而不是单纯使用auto
  • 如果函数声明使用了概念,它会自动成为一个函数模板。因此,编写函数模板就像编写普通函数一样简单。

# 问题65:C++中有哪些可用的标准属性(attributes)?

首先,什么是属性?它看起来是什么样的?

一个简单的属性看起来是这样的:[[attribute]]。但它可以有参数(如[[deprecated("because")]]),也可以有命名空间(如[[gnu::unused]]),或者两者都有。

属性为实现定义的语言扩展(如GNU和IBM的语言扩展)提供了统一的标准语法。它们几乎可以在C++程序的任何地方使用,但我们今天关注的不是这些。

我们感兴趣的是C++标准定义的属性。C++11引入了第一批标准属性:

  • [[noreturn]]表示函数不会返回。这并不意味着它返回void,而是根本不会返回。这可能意味着它总是抛出异常,根据输入不同,抛出的异常也可能不同。
  • [[carries_dependency]]表示在release-consumestd::memory_order内存序下,函数内外的依赖链会传播,这使得编译器可以跳过不必要的内存屏障指令。

然后,C++14添加了另一种类型的属性,有两个版本:

  • [[deprecated]][[deprecated(reason)]]表示不鼓励使用该实体,可以通过参数指定原因。

C++17加快了脚步,又添加了三个属性:

  • [[fallthrough]]switch-case语句中表示故意省略了breakreturn。从一个case标签直接跳到下一个是有意为之。
  • [[nodiscard]]表示函数的返回值不应该被丢弃,换句话说,必须将其保存到一个变量中,否则会得到编译器警告。
  • [[maybe_unused]]抑制对未使用实体的编译器警告。例如,如果一个变量声明时使用了[[maybe_unused]],即使它未被使用,也不会得到编译器警告。

C++20又添加了4个属性:

  • [[nodiscard("reason")]][[nodiscard]]相同,但指定了原因。
  • [[likely]]向编译器表明,switch-caseif-else分支中某个分支比其他分支更有可能被执行,这样编译器可以针对该求值路径进行优化。
  • [[unlikely]][[likely]]概念相同,但在这种情况下,标记的路径比其他路径更不可能被执行。
  • [[no_unique_address]]表示这个数据成员不必具有与它所在类的其他非静态数据成员不同的地址。

# 问题66:什么是三路比较(3-way comparison)?

三路比较运算符也被称为太空船运算符,看起来是这样的:lhs <=> rhs

它有助于判断哪个操作数更大、更小,或者它们是否相等。

如果你在C++中实现过比较运算符,就会知道这是一项多么单调乏味的任务。你必须定义六个运算符(==!=<<=>>=)。

有了C++20,你只需为太空船运算符使用=default,它就会为你生成所有六个constexprnoexcept的运算符,并且这些运算符会执行字典序比较。

关于支持类型的具体规则,请查看CppReference。

# 问题67:解释 consteval 和 constinit 为C++带来了什么?

C++11引入了constexpr表达式,它可能在编译时求值。

C++20引入了两个与之相关的新关键字:constevalconstinit

consteval可以用于函数:

consteval  int  sqr(int  n) {
    return  n * n;
}
1
2
3

consteval函数保证在编译时执行,因此它们会创建编译时常量。它们不能分配或释放数据,也不能与静态或线程局部变量交互。

constinit可以应用于具有静态存储期或线程存储期的变量。所以局部变量或成员变量不能使用constinit。它保证变量的初始化在编译时进行。

需要注意的是,constexprconst变量一旦赋值就不能更改,而constinit变量不是常量,它的值可以改变。

# 问题68:什么是模块(modules)?它们有什么优点?

正如我们已经讨论过的,#include语句基本上是文本包含。预处理器宏会用要包含文件的内容替换#include语句。

因此,一个简单的“Hello World”程序可能会从大约100字节增长到13000字节,仅仅是因为包含了<iostream>

即使你只想使用其中一个小函数,所有的头文件内容都会被复制。

C++20引入的模块最终提供了解决方案。导入一个模块基本上没有开销,与包含头文件不同,导入的顺序也无关紧要。

有了模块,你可以轻松构建自己的库,通过export限定符,你可以轻松决定哪些内容要暴露,哪些不要。

多亏了模块,不再需要将头文件和实现文件分开。

下面是一个简短的示例:

// math.cppm
export  module  math;

export  int  square(int  n){
    return  n*n;
}

// main.cpp
import  math;

int  main(){
    square(42);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

想要了解更多细节(有很多!),可以参考《C++20完全指南 (opens new window)》。

上次更新: 2025/03/27, 20:29:48

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>