三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

C++20(上)

C++20(上)

一、概念和约束

概念(concept)是C++20引入的模板参数约束机制,它允许程序员明确指定模板参数必须满足的条件。概念本质上是⼀个编译时谓词,用于验证模板参数是否满足特定要求。概念会在模板实例化前
检查类型是否满足条件,而不是在实例化后产生难以理解的错误。

1.1 基本概念及定义

在C++20中,使用关键字concept来定义概念。基本语法如下:

template<template-parameter-list>

concept concept-name = constraint-expression;

其中,template-parameter-list 是模板参数列表,concept-name 是概念名,constraint-expression 是一个可以被求值的bool类型的常量表达式(通常是各种约束组合)。

// 方式1:使用标准库类型萃取 template<typename T> concept Integral = std::is_integral_v<T>; // 方式2:常量表达式 template <typename T> concept SmallType = sizeof(T) <= 4;

此外还有一种方式:requires表达式。基本语法如下:

template<typename T>
concept ConceptName = requires (parameter-list)

{
requirement-seq
};

// 方式3:requires子句 template <typename T> concept Incrementable = requires (T t) { t++; // 检查后置++是否有效 ++t; // 检查前置++是否有效 };

1.2 requires表达式的四种形式

1.2.1 简单要求

最简单的形式,只检查表达式是否合法(能否编译通过)

template <typename T> concept Addable = requires (T a, T b) { a + b; // 判断类型T是否支持+运算 };

1.2.2 类型要求

检查某个嵌套类型是否存在

template <typename T> concept HasValueType = requires { typename T::iterator; // 检查T是否有iterator嵌套类型 typename T::value_type; // 检查T是否有value_type嵌套类型 };

1.2.3 复合要求

不仅检查表达式是否合法,还可以指定返回类型和异常规范

复合要求的完整语法:{ 表达式 } [ noexcept ] -> 类型约束;

template <typename T> concept ConvertibleAddable = requires (T a, T b) { { a + b }->std::convertible_to<T>; // 要求a+b的结果可转换为T { a += b }noexcept->std::same_as<T&>; // 要求a+=b不抛异常且返回T& };

1.2.4 嵌套要求

在 requires 内部再使用 requires 进行更复杂的约束

requires (参数列表)

{
... 其他要求 (如类型要求、复合要求等)
...requires <常量布尔表达式>;

}

template<typename T> concept ComplexConcept = requires (T t) { requires sizeof(T) <= sizeof(long); requires std::is_class_v<T>; };

由于requires表达式的结果也是一个常量表达式,所以还有下面这种进阶的嵌套写法:

template<typename T> concept ComplexConcept = requires (T t) { requires std::is_class_v<T>; requires requires (T x) { x.size(); x + x; }; };

1.3 约束的使用方式

template<class T> concept Integral = std::is_integral_v<T>; template<class T> concept Float = std::is_floating_point_v<T>;

1.3.1 模板参数后直接使用

// 1.模板参数后直接使用 template<Integral T> void f1(T x){}

1.3.2 requires子句

// 2.requires子句 template<class T> requires Integral<T> || Float<T> void f2(T x){}

上面这段代码还可以改成尾置形式:

// 3.尾置requires子句 template<class T> void f3(T x) requires Integral<T> || Float<T> {}

1.3.3 临时约束

使用临时约束时就不用再定义概念了,它用的是requires requires子句。

// 4.临时约束--使用requires requires template<class T> requires requires(T x) { x.size(); x + x; } void f4(T x){}

1.3.4 auto占位符约束

前面说过,C++17允许使用auto作为非类型模板参数的占位符。

// 5.auto占位符约束 void f5(Integral auto x){}

1.4 约束的类型

1、原子约束:是不可再分的约束表达式,如一个概念名std::integral<T> ,一个requires表达式,一个常量布尔表达式等。

2、逻辑与(合取):所有约束都必须为 true,整个合取约束才为 true,使用 && 运算符连接多个原子约束。

3、逻辑或(析取):多个约束中有一个为 true ,整个析取约束就为 true ,使用 || 运算符连接多个约束。

// 1. 逻辑与(&&) template<typename T> concept IntegralAndPrintable = std::integral<T> && requires(T t) { std::cout << t; }; // 2. 逻辑或(||) template<typename T> concept Number = std::integral<T> || std::floating_point<T>; // 3. 逻辑非(!) template<typename T> concept NonPointer = !std::is_pointer_v<T>;

1.5 约束的偏序规则

C++20概念的约束偏序规则用于在函数重载或模板特化时,判断哪个约束的模板"更特化"或"更受
约束"。编译器使用这个规则来选择最合适的模板。

偏序规则的核心:如果约束 A 包含(蕴含)约束 B,那么 A比B "更受约束"。这意味着任何满足A 的类型也⼀定会满足 B,但满足 B 的类型不⼀定满足 A。即 A ⇒ B (A 蕴含 B)。当两个模板匹配时,编译器会选择"更受约束"的那一个

// 定义⼀个要求T是整形的概念 template<class T> concept Integral = std::is_integral_v<T>; template<Integral T> void f(T x) { std::cout << "有 concepts 约束" << std::endl; } template<class T> void f(T x) { std::cout << "无 concepts 约束" << std::endl; } int main() { // 符合concept要求则实例化调用对应的函数模板 f(1); // 有 concepts 约束 // 不符合concept要求,查找无concept要求的模板实例化调用,没找到则报错 f("xxx"); // 无 concepts 约束 }

1.6 SFINAE和Concepts

SFINAE是Substitution Failure Is Not An Error的缩写,翻译为“替换失败不是错误”,通过模板替换失败静默排除候选函数,实现编译期条件分支,依赖模板实例化规则如std::enable_if、decltype等。

SFINAE代码冗长且晦涩,Concepts是SFINAE的官方替代方案,解决了在可维护性和表达力上的缺陷。

// C++17:使用enable_if约束整型参数 template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>> void foo(T x) {} // C++20:使用concept约束 template<std::integral T> void foo(T x) {}

二、模块

C++20引入的模块(Modules)是C++语言的一项重大更新,旨在解决传统头文件包含机制(#include)的诸多问题。

传统头文件包含的问题:
• 编译时间长:每次包含头文件时,编译器都需要重新解析其内容,导致编译时间大幅增加。
• 依赖管理复杂:头文件的依赖关系可能导致复杂的包含顺序问题,容易引发错误。
• 命名空间污染:头文件中声明的符号可能会意外地进⼊全局命名空间,导致命名冲突。
模块是⼀种新的代码组织方式,具有以下特点:
• 编译⼀次,多次使用:模块接口只编译⼀次,生成⼆进制表示供后续使用
• 隔离性:模块内部细节默认对外不可见
• 无宏泄漏:模块内的宏定义不会影响导入者
• 更快的编译:避免了重复解析相同的头文件
• 更清晰的语义:明确区分接口和实现

2.1 模块的基本语法

和之前类似,模块分为声明文件实现文件。声明文件后缀⼀般为.ixx或者.cppm,实现文件一般为.cpp。

模块声明文件开头添加export module 模块名。声明文件中需要给外部使用的类型/函数/变量等需要用export 声明导出,否则外部不能使用;模块实现文件开头添加module 模块名;模块使用文件中import 模块名

模块声明:

// math.ixx #include <iostream> export module math; // 导出函数hello export char const* hello() { return "hello"; } // 函数world未被导出,外部不能使用,内部才能使用 char const* world() { return "world"; } // 多个函数一起导出 export { int one() { return 1; } int zero() { return 0; } } // 导出命名空间中的内容 export namespace zyc { int add(int a, int b); } // 模板类似以前的处理,⼀般定义到声明⽂件 // 声明和定义分离会导致链接错误,需要显⽰实例化解决 export template<class T> void TFunc(const T& x) { std::cout << x << std::endl; } // 导出类 export class A { public: void f1(); private: int _a1 = 1; };

模块实现:

// math.cpp #include <iostream> module math; namespace zyc { int add(int x, int y) { return x + y; } } void A::f1() { std::cout << "hello world" << std::endl; }

模块使用:

// test.cpp import math; #include <iostream> int main() { std::cout << hello() << std::endl; std::cout << one() << std::endl; std::cout << zero() << std::endl; std::cout << zyc::add(1, 2) << std::endl; TFunc(10); A().f1(); return 0; }

C++20 模块的设计目标是让代码的导⼊和导出更加高效和清晰,为了实现这⼀点,模块单元在语法上受到严格限制,在⼀个模块单元中,你只能有 module 和 import 指令,不能有#include 或其他预处理指令。

但是这样也就导致了另⼀个很麻烦的问题,很多标准库或者第三方库等还没有模块化,那么我们的项目模块化了,但是就无法跟他们兼容使用。所以这里要引出一个解决方案:全局模块片段。它为你提供了⼀个“安全区”,可以在模块单元正式内容开始之前,放置这些必须的预处理指令。

具体形式如下:

module; // 开启全局模块⽚段 // 只能有预处理指令。任何⾮预处理指令的 C++ 代码都会导致编译错误。 #include <iostream> #include "third_party/opengl.h" #define OLD_MACRO 42 // 全局模块⽚段到此结束 export module MyModule; // 模块声明,标志着全局模块⽚段的结束

2.2 模块的分区

模块分区是C++20模块系统中的⼀个重要特性,它允许将⼤型模块拆分为多个逻辑单元,同时保持模块的封装性和⼀致性,便于管理大型模块且能减少编译依赖,可以独立编译分区。

分区声明export module 模块名:分区名;

分区实现module 模块名:分区名;

分区导入import :分区名;

分区导出(主模块中)export import :分区名;

export module math:algebra; // 声明math模块的algebra分区 module math:algebra; // 实现math模块的algebra分区 export module math:geometry; import :algebra; // 导⼊同⼀模块的algebra分区 export module math; export import :algebra; // 导出algebra分区

2.3 模块的优势

头文件的问题:1、重复解析:每个翻译单元(.cpp⽂件)包含头⽂件时,都需要重新解析和编译头⽂件内容。例如, <vector> 在100个⽂件中被包含,就会被解析100次。2、宏和模板膨胀:头⽂件中的宏和模板会在每个包含它的翻译单元中展开,增加编译开销。

模块的优势:1、⼀次性编译:模块接口( .ixx 或 .cppm )只需编译⼀次,⽣成⼆进制模块接⼝,后续导⼊直接复⽤预处理结果,编译速度提升。2、隔离性:模块只导出显式声明的内容,避免宏污染和隐式依赖。

← 返回列表