30、C++20 其他重要特性:std::format、std::span、std::jthread、日历与时区库
C++20 带来的好东西实在太多了。除了 Concepts、Ranges、Coroutines 这些大块头,还有几个“小而美”的特性,日常开发中特别实用。今天咱们就聊聊这四个:std::format、std::span、std::jthread,以及日历与时区库。
说实话,这几个特性我盼了很久。尤其是 std::format,以前写日志拼接字符串,那叫一个痛苦。现在好了,终于可以像 Python 那样优雅地格式化输出了。
std::format:终于等到你
我最早接触格式化字符串是在 Python 里,后来写 Go 也用 fmt.Sprintf。回到 C++ 后,总觉得 sprintf 不安全,std::stringstream 又太啰嗦。C++20 的 std::format 算是把这个缺口补上了。
它的语法和 Python 的 f-string 很像。用花括号 {} 做占位符,后面跟参数。看个例子:
#include <format>
#include <iostream>
int main() {
std::string name = "Alice";
int age = 30;
double score = 92.5;
// 基本用法
std::string msg = std::format("姓名: {}, 年龄: {}, 分数: {}", name, age, score);
std::cout << msg << std::endl;
// 指定格式:保留两位小数
msg = std::format("分数: {:.2f}", score);
std::cout << msg << std::endl;
// 对齐和填充
msg = std::format("{:<10} {:>10}", "左对齐", "右对齐");
std::cout << msg << std::endl;
return 0;
}
std::format。以前用 snprintf 总担心缓冲区溢出,现在类型安全,心里踏实多了。
你可能会问:那和 fmtlib 库有什么关系?其实 C++20 的 std::format 就是基于 fmtlib 的。如果你还在用 C++17,可以直接用 fmtlib,等升级到 C++20 后无缝切换到标准库版本。
格式化语法支持很多花样:
- 位置参数:
std::format("{1} {0}", "world", "hello")输出 "hello world" - 进制输出:
std::format("{:x}", 255)输出 "ff" - 千位分隔:
std::format("{:L}", 1234567)输出 "1,234,567"(需要本地化支持) - 自定义类型:可以特化
std::formatter来格式化自己的类
std::format 的支持不完整。我曾在 GCC 12 上踩过坑,编译报错说找不到头文件。建议用 GCC 13+ 或 Clang 16+。
std::span:不拥有数据的“视图”
写过 C 的朋友对指针和数组长度一定不陌生。传一个数组给函数,通常要同时传长度,一不小心就传错了。C++20 的 std::span 就是来解决这个问题的。
std::span 本质上是一个轻量级的“视图”,它不拥有数据,只保存指针和长度。它可以引用连续内存区域,比如数组、std::vector、std::array 等。
#include <span>
#include <vector>
#include <iostream>
void process_data(std::span<int> data) {
for (auto& v : data) {
v *= 2; // 可以修改原数据
}
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
process_data(vec); // 直接传 vector,自动转 span
int arr[] = {10, 20, 30};
process_data(arr); // 数组也可以
for (auto v : vec) std::cout << v << " "; // 输出 2 4 6 8 10
return 0;
}
你看,函数签名里不需要再写两个参数了。一个 std::span 搞定一切。而且它支持范围 for 循环,用起来很自然。
std::span 不拥有数据,不负责释放内存。它只是一个“窗口”,让你安全地访问连续内存。这和 std::string_view 的思路一脉相承。
我在项目中遇到过一个问题:某个函数需要处理 std::vector<int>,但调用方有时传的是 std::array,有时是裸数组。以前得写重载,或者强制转成 vector(有拷贝开销)。用了 std::span 后,一个函数通吃所有连续容器。
std::span 还支持动态长度和固定长度两种模式:
std::span<int>:动态长度,运行时确定std::span<int, 5>:固定长度,编译期确定,性能更好
std::span 存下来,等原数据销毁后再去访问,结果当然是未定义行为。记住,std::span 不延长数据生命周期,用的时候要保证原数据还活着。
std::jthread:自动 join 的线程
多线程编程中,最烦人的事情之一就是忘记 join 或 detach。C++11 的 std::thread 如果析构时还没处理,程序直接 terminate。C++20 的 std::jthread 解决了这个痛点。
jthread 的全称是 "joining thread"。它的析构函数会自动调用 join(),确保线程安全退出。另外,它还支持“停止令牌”(std::stop_token),可以优雅地请求线程停止。
#include <thread>
#include <iostream>
#include <chrono>
void worker(std::stop_token st) {
while (!st.stop_requested()) {
std::cout << "工作中..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "收到停止信号,退出" << std::endl;
}
int main() {
std::jthread t(worker); // 自动管理生命周期
std::this_thread::sleep_for(std::chrono::seconds(2));
t.request_stop(); // 请求停止
// t 析构时自动 join,无需手动调用
return 0;
}
这个例子中,worker 函数检查 stop_token,如果收到停止请求就退出。主线程调用 request_stop() 后,线程会优雅地结束。即使不调用 request_stop(),jthread 析构时也会自动 join。
std::jthread 替代 std::thread。它几乎零成本,却省去了手动管理线程生命周期的麻烦。尤其是异常安全方面,jthread 的 RAII 特性让代码更健壮。
你可能会想:那 std::thread 是不是该退休了?其实也不一定。如果你需要更精细的控制,比如分离线程(detach),jthread 并不支持。但大多数场景下,jthread 是更好的选择。
日历与时区库:终于不用自己算日期了
这个特性是 <chrono> 库的扩展。C++11 的 chrono 只有时间点和时长,没有日期和时区。C++20 补上了这个短板。
日历库支持年、月、日、星期等概念。时区库则提供了 IANA 时区数据库的支持。看个例子:
#include <chrono>
#include <iostream>
using namespace std::chrono;
int main() {
// 创建一个日期:2024年12月25日
auto christmas = 2024y / December / 25d;
std::cout << "圣诞节: " << christmas << std::endl;
// 获取当前时间
auto now = system_clock::now();
auto today = floor<days>(now);
std::cout << "今天: " << year_month_day{today} << std::endl;
// 计算两个日期之间的天数
auto diff = sys_days{christmas} - today;
std::cout << "距离圣诞节还有: " << diff.count() << " 天" << std::endl;
// 时区转换
auto utc_time = system_clock::now();
auto local_time = zoned_time{"Asia/Shanghai", utc_time};
std::cout << "上海时间: " << local_time << std::endl;
return 0;
}
这个例子展示了几个关键点:
- 日期字面量:
2024y、December、25d,读起来像自然语言 - 日期计算:可以直接相减得到天数
- 时区支持:
zoned_time自动处理时区偏移
日历库还支持很多操作:
- 获取星期几:
year_month_weekday可以算出某个月的第几个星期几 - 最后一天:
last表示某月的最后一天,比如2024y / February / last - 时区列表:
get_tzdb().zones可以列出所有可用时区
说实话,这个库刚出来时我还有点不习惯。毕竟以前用 time_t 和 struct tm 习惯了。但用了几次之后,就再也回不去了。类型安全、表达力强,这才是现代 C++ 该有的样子。
好了,这四个特性就聊到这里。它们虽然不像 Concepts 或 Ranges 那样“炸裂”,但每一个都解决了实际开发中的痛点。尤其是 std::format 和 std::jthread,我几乎每天都在用。如果你还没试过,强烈建议上手体验一下。