Ch 28: C++20 其他现代特性
- 理解 consteval 强制编译期计算
- 掌握 std::format 格式化
- 熟练使用 std::span
- 掌握三路比较运算符
<=> - 了解 C++20 其他实用特性
28.1 consteval - 强制编译期计算
Section titled “28.1 consteval - 强制编译期计算”Python 无编译期计算
Section titled “Python 无编译期计算”# Python - 所有计算在运行时def factorial(n): result = 1 for i in range(2, n + 1): result *= i return result
x = factorial(10) # 运行时计算print(x) # 3628800C++ consteval vs constexpr
Section titled “C++ consteval vs constexpr”#include <iostream>
// constexpr - 可以是编译期或运行期constexpr int fib(int n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2);}
int main() { // fib(10) 可能在编译期计算 int x = fib(10); // 编译器决定:编译期或运行期
// 运行时输入 // int n; std::cin >> n; // fib(n) // 保证运行期计算
std::cout << x << std::endl;}consteval 强制编译期
Section titled “consteval 强制编译期”#include <iostream>
// consteval - 强制编译期求值consteval int factorial(int n) { int result = 1; for (int i = 2; i <= n; ++i) { result *= i; } return result;}
// 编译期验证static_assert(factorial(5) == 120); // 编译时验证!
int main() { // 编译期计算,结果直接嵌入 int x = factorial(10); // 编译期计算,3628800 直接作为常量 std::cout << x << std::endl;
// factorial(10.5); // 编译错误!必须是整数常量表达式
// 运行时值不能传给 consteval // int n; std::cin >> n; // factorial(n); // 编译错误!n 不是常量表达式}consteval vs constexpr 对比
Section titled “consteval vs constexpr 对比”#include <iostream>#include <array>
// constexpr:编译期或运行期constexpr int comp_sum(int a, int b) { return a + b;}
// consteval:必须是编译期consteval int const_sum(int a, int b) { return a + b;}
int main() { // 两者都可以用于常量上下文 static_assert(comp_sum(1, 2) == 3); static_assert(const_sum(1, 2) == 3);
// 差异:运行时参数 int a = 1, b = 2; int r1 = comp_sum(a, b); // OK,编译期或运行期 // int r2 = const_sum(a, b); // 错误!参数必须是常量
constexpr int ca = 1, cb = 2; int r3 = const_sum(ca, cb); // OK,constexpr 参数是常量}
// consteval 用于元编程和性能关键代码consteval int square(int x) { return x * x; }consteval int cube(int x) { return x * x * x; }
// 编译期计算数组大小consteval std::size_t array_size() { return 10; }std::array<int, array_size()> arr; // 编译期确定大小consteval 实际应用
Section titled “consteval 实际应用”#include <iostream>#include <array>#include <type_traits>
// consteval 用于类型计算consteval int fahrenheit_to_celsius(int f) { return (f - 32) * 5 / 9;}
consteval bool is_little_endian() { int x = 1; return *reinterpret_cast<char*>(&x) == 1;}
// 编译期验证 endiannessstatic_assert(is_little_endian(), "Big endian not supported");
// consteval 用于 assert(在编译期检查)consteval bool in_range(int v, int min, int max) { return v >= min && v <= max;}
template<int N>struct Factorial { static constexpr int value = N * Factorial<N - 1>::value;};
template<>struct Factorial<0> { static constexpr int value = 1;};
// 编译期计算阶乘static_assert(Factorial<5>::value == 120);static_assert(Factorial<10>::value == 3628800);
int main() { std::cout << "Factorial 10: " << Factorial<10>::value << "\n";
// 编译期温度转换 constexpr int freezing_f = 32; constexpr int freezing_c = fahrenheit_to_celsius(freezing_f); static_assert(freezing_c == 0);
std::cout << "Freezing point: " << freezing_c << "C\n";}28.2 std::format 格式化
Section titled “28.2 std::format 格式化”#include <format>#include <iostream>#include <string>
int main() { // 基本格式化 std::string s = std::format("Hello, {}!", "World"); std::cout << s << std::endl; // Hello, World!
// 多个占位符 std::string s2 = std::format("{} + {} = {}", 1, 2, 3); std::cout << s2 << std::endl; // 1 + 2 = 3
// 指定位置 std::string s3 = std::format("{0} {1} {0}", "Hello", "World"); std::cout << s3 << std::endl; // Hello World Hello
// 命名参数(C++20 不支持,需要 C++23 或手写) // std::string s4 = std::format("name={name}", std::format_arg("name", "Alice"));}#include <format>#include <iostream>#include <iomanip>
int main() { // 浮点数精度 std::cout << std::format("PI = {:.4f}\n", 3.141592653); // PI = 3.1416 std::cout << std::format("PI = {:.2f}\n", 3.14159); // PI = 3.14
// 十六进制 std::cout << std::format("Hex: {:#x}\n", 255); // Hex: 0xff std::cout << std::format("Hex: {:x}\n", 255); // Hex: ff std::cout << std::format("Hex: {:02x}\n", 255); // Hex: ff
// 二进制 std::cout << std::format("Binary: {:b}\n", 42); // Binary: 101010
// 八进制 std::cout << std::format("Octal: {:o}\n", 64); // Octal: 100
// 科学计数法 std::cout << std::format("Scientific: {:.2e}\n", 123456.789); // 1.23e+05}#include <format>#include <iostream>
int main() { // 右对齐(默认) std::cout << std::format("{:>10}\n", "right"); // right
// 左对齐 std::cout << std::format("{:<10}\n", "left"); // left
// 居中 std::cout << std::format("{:^10}\n", "center"); // center
// 填充字符 std::cout << std::format("{:*>10}\n", "pad"); // *******pad std::cout << std::format("{:*<10}\n", "pad"); // pad******* std::cout << std::format("{:*^10}\n", "pad"); // ***pad****
// 数字对齐 std::cout << std::format("{:+10}\n", 42); // +42 std::cout << std::format("{:<+10}\n", 42); // +42}#include <format>#include <iostream>
int main() { // 显示正负号 std::cout << std::format("{:+d}\n", 42); // +42 std::cout << std::format("{:+d}\n", -42); // -42
// 前导零 std::cout << std::format("{:05d}\n", 42); // 00042 std::cout << std::format("{:010d}\n", 42); // 0000000042
// 分组分隔符(C++23) // std::cout << std::format("{:,}\n", 1234567); // 1,234,567
// 百分比 std::cout << std::format("{:.1%}\n", 0.75); // 75.0%
// 科学计数法 std::cout << std::format("{:.2e}\n", 123456.789); // 1.23e+05}#include <format>#include <iostream>#include <vector>#include <string>
struct Person { std::string name; int age; double height;};
int main() { // 结构体格式化 Person p{"Alice", 30, 165.5}; std::cout << std::format("Name: {:10s} Age: {:3d} Height: {:.1f}\n", p.name, p.age, p.height); // Name: Alice Age: 30 Height: 165.5
// 表格输出 std::vector<Person> people = { {"Alice", 30, 165.5}, {"Bob", 25, 180.0}, {"Charlie", 35, 170.2} };
std::cout << std::format("{:<10} {:>4} {:>6}\n", "Name", "Age", "Height"); std::cout << std::string(22, '-') << "\n"; for (const auto& person : people) { std::cout << std::format("{:<10} {:>4} {:>6.1f}\n", person.name, person.age, person.height); }}28.2.5 C++20 Modules(模块)
Section titled “28.2.5 C++20 Modules(模块)”传统头文件的问题
Section titled “传统头文件的问题”// 传统 #include 的问题:// 1. 编译速度慢 - 每次都要解析整个头文件// 2. 宏污染 - 头文件中的宏会影响整个翻译单元// 3. 重复编译 - 同一个头文件被多次解析
// foo.h#ifndef FOO_H#define FOO_H#define MAX_SIZE 100 // 可能与其他头文件冲突class Foo { /* ... */ };#endif
// main.cpp#include "foo.h" // 展开整个 foo.h#include "bar.h" // 可能也定义了 MAX_SIZE// ===== 模块定义文件 mathModule.cpp =====// 注意:模块文件名和模块名不必相同
module; // 可选,表示这是模块接口
export module math; // 导出模块声明
// 私有模块片段(不影响外部可见性)module privately;
// 导出接口export int add(int a, int b) { return a + b;}
export constexpr int MAX_VALUE = 100;
// 非导出(模块内部)namespace internal { int helper(int x) { return x * 2; }}
// ===== 使用模块的代码 =====// main.cppimport math; // 导入模块,而不是 #include
int main() { int result = add(1, 2); // 直接使用,无歧义 // add 在 math 模块中定义,不存在宏污染}模块与头文件对比
Section titled “模块与头文件对比”// ===== 头文件方式 =====#ifndef UTILS_H#define UTILS_H#define MAX 100 // 全局宏污染
inline int square(int x) { return x * x; } // 每次#include都重新实例化
struct Data { int value; };#endif
// user1.cpp#include "utils.h" // 展开 MAX,引入 square
// user2.cpp#include "utils.h" // 再次展开,重复编译
// ===== 模块方式 =====// mathModule.cppmodule;export module math;
export inline int square(int x) { return x * x; }export constexpr int MAX = 100;
// user1.cppimport math; // 只导入一次
// user2.cppimport math; // 共享模块接口,零重复编译// 主模块export module foo;
// foo 命名空间(模块内部)export namespace foo { void public_func();}
namespace foo { void private_func(); // 模块内部}
// 分区模块export module foo:detail; // foo 的 detail 分区
export void hidden_helper(); // 仅模块内部使用
// 使用import foo;import foo:detail; // 需要时显式导入分区现状和兼容性
Section titled “现状和兼容性”// ===== 编译器支持现状(2024) =====
// GCC 11+ - 基本支持// Clang 15+ - 基本支持// MSVC 19.29+ - 基本支持
// 注意:std 模块尚未标准化// import std; // 大多数编译器不支持
// 当前最佳实践:// - 模块主要用于大型项目加速编译// - 标准库仍使用 #include// - 混用头文件和模块是安全的
// 示例:混用export module myapp; // 项目自定义模块import std; // 标准库模块(未来)#include <spdlog/spdlog.h> // 第三方库(当前只能头文件)模块实现建议
Section titled “模块实现建议”// ===== 推荐项目结构 =====
/project├── src/│ ├── main.cpp│ ├── myapp.cpp // 模块实现│ └── myapp.cppm // 模块接口(.cppm 是常用扩展名)└── include/ └── myapp.h // 仍需要传统头文件(对外 API)
// ===== myapp.cppm =====module;export module myapp;
export import std.core; // 未来(C++26)// import std; // C++26 目标
export { void init(); int process(int);}
// ===== 迁移策略 =====// 1. 新代码用模块// 2. 渐进式迁移现有头文件// 3. 库优先提供模块接口(.cppm)// 4. 头文件保持向后兼容
// 推荐工具:// - cmake 的 CMakeLists.txt 支持 modules// - build2 工具对 modules 支持较好28.3 std::span - 非拥有视图
Section titled “28.3 std::span - 非拥有视图”#include <span>#include <vector>#include <array>#include <iostream>
// span - 连续内存的轻量视图void print_span(std::span<int> data) { for (int x : data) { std::cout << x << " "; } std::cout << "\n"; std::cout << "size: " << data.size() << "\n";}
int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; std::array<int, 5> arr = {1, 2, 3, 4, 5}; int c_arr[] = {1, 2, 3, 4, 5};
// 从不同容器创建 span print_span(vec); // 1 2 3 4 5 print_span(arr); // 1 2 3 4 5 print_span(c_arr); // 1 2 3 4 5
// 空 span std::span<int> empty; std::cout << "empty.size(): " << empty.size() << "\n"; // 0}#include <span>#include <vector>#include <iostream>
int main() { std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::span<int> sp(vec);
// first(n) - 前 n 个元素 auto first3 = sp.first(3); std::cout << "first(3): "; for (int x : first3) std::cout << x << " "; // 1 2 3 std::cout << "\n";
// last(n) - 后 n 个元素 auto last3 = sp.last(3); std::cout << "last(3): "; for (int x : last3) std::cout << x << " "; // 8 9 10 std::cout << "\n";
// subspan(offset, count) - 子范围 auto sub = sp.subspan(2, 4); // 从索引2开始,4个元素 std::cout << "subspan(2, 4): "; for (int x : sub) std::cout << x << " "; // 3 4 5 6 std::cout << "\n";
// 使用数组指针和大小创建 int* data = vec.data(); std::span<int> from_ptr(data + 1, 3); // 2, 3, 4 std::cout << "from_ptr: "; for (int x : from_ptr) std::cout << x << " "; std::cout << "\n";}span 作为函数参数
Section titled “span 作为函数参数”#include <span>#include <vector>#include <array>#include <iostream>
// 优势:接受多种容器类型int sum_elements(std::span<const int> data) { int sum = 0; for (int x : data) { sum += x; } return sum;}
// 处理子范围void process_window(std::span<int> window) { for (auto& x : window) { x *= 2; }}
int main() { std::vector<int> vec = {1, 2, 3, 4, 5}; std::array<int, 5> arr = {1, 2, 3, 4, 5}; int raw[] = {1, 2, 3, 4, 5};
// 统一接口 std::cout << sum_elements(vec) << "\n"; // 15 std::cout << sum_elements(arr) << "\n"; // 15 std::cout << sum_elements(raw) << "\n"; // 15
// 处理原始数组的子范围 std::span<int> sp(vec); process_window(sp.subspan(1, 3)); // 处理 2, 3, 4
std::cout << "After: "; for (int x : vec) std::cout << x << " "; // 1 4 6 8 5 std::cout << "\n";}span vs 指针+大小
Section titled “span vs 指针+大小”#include <span>#include <vector>#include <iostream>
// 旧式 APIvoid old_api(const int* data, std::size_t size) { for (std::size_t i = 0; i < size; ++i) { std::cout << data[i] << " "; } std::cout << "\n";}
// 现代 APIvoid modern_api(std::span<const int> data) { for (int x : data) { std::cout << x << " "; } std::cout << "\n";
// span 知道大小 if (!data.empty()) { std::cout << "First: " << data.front() << "\n"; std::cout << "Last: " << data.back() << "\n"; }
// 边界安全(C++20) // data[100]; // 运行时断言失败}
int main() { std::vector<int> vec = {1, 2, 3, 4, 5};
// 旧式调用 old_api(vec.data(), vec.size());
// 现代调用 modern_api(vec);
// span 自动构造 modern_api(vec.subspan(1, 3)); // 2, 3, 4}28.4 三路比较运算符 <=>
Section titled “28.4 三路比较运算符 <=>”#include <compare>#include <iostream>
int main() { // 三路比较返回 std::partial_ordering, std::weak_ordering, 或 std::strong_ordering // strong_ordering: 相似于 ==,但值可以不同(如大小写不同的字符串) // weak_ordering: 相似于 ==,忽略大小写等 // partial_ordering: 支持无序(如 NaN)
// spaceship 运算符 <=> std::cout << (1 <=> 1) << "\n"; // 0 (equal) std::cout << (1 <=> 2) << "\n"; // <0 (less) std::cout << (2 <=> 1) << "\n"; // >0 (greater)
// 使用预定义值 auto r1 = 1 <=> 2; std::cout << (r1 < 0) << "\n"; // 1 (true)
// 浮点数比较(partial_ordering) double a = 1.0, b = 2.0; auto r2 = a <=> b; if (r2 < 0) std::cout << "a < b\n";}自动生成比较运算符
Section titled “自动生成比较运算符”#include <compare>#include <iostream>
class Version {public: Version(int major, int minor, int patch) : major_(major), minor_(minor), patch_(patch) {}
// C++20:= default 自动生成所有比较运算符 auto operator<=>(const Version&) const = default;
private: int major_, minor_, patch_;};
int main() { Version v1{1, 2, 3}; Version v2{1, 2, 3}; Version v3{1, 3, 0}; Version v4{2, 0, 0};
// 所有比较自动生成 std::cout << (v1 == v2) << "\n"; // 1 (true) std::cout << (v1 != v3) << "\n"; // 1 (true) std::cout << (v1 < v3) << "\n"; // 1 (true, minor < minor) std::cout << (v1 <= v3) << "\n"; // 1 (true) std::cout << (v4 > v1) << "\n"; // 1 (true, major > major)}自定义比较逻辑
Section titled “自定义比较逻辑”#include <compare>#include <iostream>#include <string>#include <cctype>
class CaseInsensitiveString {public: explicit CaseInsensitiveString(std::string s) : s_(std::move(s)) {}
// 自定义三路比较 auto operator<=>(const CaseInsensitiveString& other) const { return case_insensitive_compare(s_, other.s_); }
private: static int case_insensitive_compare(const std::string& a, const std::string& b) { auto to_lower = [](unsigned char c) { return std::tolower(c); }; std::string la(a.size(), ' '), lb(b.size(), ' '); std::transform(a.begin(), a.end(), la.begin(), to_lower); std::transform(b.begin(), b.end(), lb.begin(), to_lower);
if (la < lb) return -1; if (la > lb) return 1; return 0; }
std::string s_;};
int main() { CaseInsensitiveString s1("Hello"); CaseInsensitiveString s2("hello"); CaseInsensitiveString s3("World");
std::cout << (s1 == s2) << "\n"; // 1 (true, case insensitive equal) std::cout << (s1 < s3) << "\n"; // 1 (true, Hello < World)}与 Python 比较
Section titled “与 Python 比较”# Python - 所有比较直观class Version: def __init__(self, major, minor, patch): self.major = major self.minor = minor self.patch = patch
def __eq__(self, other): return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __lt__(self, other): return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
# Python 3 支持元组比较v1 = (1, 2, 3)v2 = (1, 3, 0)print(v1 < v2) # True28.5 初始化语句增强
Section titled “28.5 初始化语句增强”if 初始化
Section titled “if 初始化”#include <map>#include <iostream>
int main() { // C++17: if 初始化 if (auto it = std::map<int, std::string>{{1, "one"}, {2, "two"}}.find(1); it != nullptr) { std::cout << "Found: " << it->second << "\n"; }
// C++20: 范围 for 初始化 std::vector<int> vec = {1, 2, 3, 4, 5}; for (auto v = vec; auto& x : v) { x *= 2; // vec 变成 {2, 4, 6, 8, 10} }
for (int x : vec) { std::cout << x << " "; } std::cout << "\n";}switch 初始化
Section titled “switch 初始化”#include <iostream>#include <optional>
enum class Mode { Read, Write, Append };
// 模拟返回值的函数std::optional<Mode> get_mode() { return Mode::Read;}
int main() { // switch 初始化(C++20) switch (auto result = get_mode(); result.value_or(Mode::Read)) { case Mode::Read: std::cout << "Mode: Read\n"; break; case Mode::Write: std::cout << "Mode: Write\n"; break; case Mode::Append: std::cout << "Mode: Append\n"; break; }}28.6 constinit
Section titled “28.6 constinit”#include <iostream>
// constinit - 保证静态初始化constinit int static_value = 42;
// 避免静态初始化顺序问题const int& get_static() { static int value = compute(); // 可能初始化顺序问题 return value;}
// constinit 确保静态变量在模块初始化时初始化static constinit int early_init = 10;
// 组合:constinit + constexprconstinit constexpr int arr[] = {1, 2, 3};
int main() { std::cout << "static_value: " << static_value << "\n"; std::cout << "early_init: " << early_init << "\n";}28.7 [[likely]] 和 [[unlikely]]
Section titled “28.7 [[likely]] 和 [[unlikely]]”#include <iostream>
int main() { // 分支提示:帮助编译器优化 for (int i = 0; i < 100; ++i) { if (i > 50) [[likely]] { std::cout << i << " "; } } std::cout << "\n";
// likely/unlikely 可以提高分支预测效率 int x = 42; if (x > 100) [[unlikely]] { std::cout << "Rare case!\n"; } else { std::cout << "Common case\n"; }}28.8 constexpr 算法和 lambda
Section titled “28.8 constexpr 算法和 lambda”#include <algorithm>#include <array>#include <iostream>
int main() { // constexpr 算法(C++20) constexpr std::array<int, 5> data = {5, 2, 8, 1, 9};
// 编译期排序 constexpr auto sorted = [] { std::array<int, 5> arr = data; std::sort(arr.begin(), arr.end()); return arr; }();
static_assert(sorted[0] == 1); static_assert(sorted[4] == 9);
// constexpr std::vector (C++20) // constexpr std::vector<int> vec{1, 2, 3}; // C++26 可能支持
std::cout << "Sorted: "; for (int x : sorted) { std::cout << x << " "; } std::cout << "\n";}28.9 推荐使用的 C++20 特性
Section titled “28.9 推荐使用的 C++20 特性”按实用性排序
Section titled “按实用性排序”// ===== 高实用性(立即可用) =====
#include <concepts>#include <ranges>#include <format>#include <span>
// 1. Concepts - 更好的模板约束template<std::integral T>T add(T a, T b) { return a + b; }
// 2. Ranges - 惰性视图和管道auto result = nums | std::views::filter([](int x) { return x > 0; }) | std::views::transform([](int x) { return x * x; });
// 3. std::format - 类型安全格式化auto s = std::format("{} + {} = {}", 1, 2, 3);
// 4. std::span - 非拥有视图void process(std::span<int> data);
// 5. constexpr 算法constexpr auto sum = std::accumulate(std::views::iota(1, 11), 0);
// 6. 初始化语句if (auto it = m.find(key); it != m.end()) { /* ... */ }
// 7. <=> 运算符class Vec { auto operator<=>(const Vec&) const = default;};
// 8. consteval - 强制编译期consteval int square(int x) { return x * x; }
// ===== 需要编译器支持(C++20 已完成但部分实现) =====
// 9. std::expected (C++23)// 10. std::flat_map (C++23)// 11. std::flat_set (C++23)// 12. ranges::to (C++23)不推荐的特性
Section titled “不推荐的特性”// 协程 - 复杂,库支持有限// #include <coroutine> // 实际项目用 libasio 或其他库
// Modules - 工具链支持不完整// import std; // 大多数编译器不支持
// 等待广泛支持后再使用这些特性28.10 章节总结
Section titled “28.10 章节总结”consteval vs constexpr:
| 特性 | consteval | constexpr |
|---|---|---|
| 求值时机 | 必须编译期 | 编译期或运行期 |
| 运行时参数 | 不支持 | 支持 |
| 用于常量上下文 | 支持 | 支持 |
std::format 占位符:
| 占位符 | 说明 | 示例 |
|---|---|---|
{} | 默认格式 | {} |
{:d} | 整数 | {:d} |
{:f} | 浮点 | {:.2f} |
{:x} | 十六进制 | {:#x} |
{:>10} | 右对齐 | {:>10} |
{:<10} | 左对齐 | {:<10} |
{:^10} | 居中 | {:^10} |
span 视图操作:
| 操作 | 说明 |
|---|---|
first(n) | 前 n 个元素 |
last(n) | 后 n 个元素 |
subspan(o,c) | 从 o 开始取 c 个 |
size() | 元素数量 |
empty() | 是否为空 |
data() | 原始指针 |
三路比较返回值:
std::strong_ordering:等价性可传递(如==)std::weak_ordering:等价性不可传递(如忽略大小写比较)std::partial_ordering:支持无序(如浮点数 NaN)
下章预告:ch29 学习错误处理,理解异常安全和 noexcept。