30、C++20 其他重要特性:std::format、std::span、std::jthread、日历与时区库

C++20 带来的好东西实在太多了。除了 Concepts、Ranges、Coroutines 这些大块头,还有几个“小而美”的特性,日常开发中特别实用。今天咱们就聊聊这四个:std::formatstd::spanstd::jthread,以及日历与时区库。

说实话,这几个特性我盼了很久。尤其是 std::format,以前写日志拼接字符串,那叫一个痛苦。现在好了,终于可以像 Python 那样优雅地格式化输出了。

C++20 其他重要特性总览 std::format 类型安全格式化 替代 sprintf / iostream 支持自定义类型 std::span 轻量级数组视图 不拥有数据 兼容连续容器 std::jthread 自动 join 的线程 支持停止令牌 RAII 管理线程 日历与时区 chrono 扩展 时区转换 日期计算 核心价值 减少样板代码 · 提升类型安全 · 简化并发编程 · 统一时间处理 这些特性让 C++ 在现代工程实践中更加顺手 —— 每个特性都解决了一个真实的痛点

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 来格式化自己的类
注意:GCC 13 之前对 std::format 的支持不完整。我曾在 GCC 12 上踩过坑,编译报错说找不到头文件。建议用 GCC 13+ 或 Clang 16+。

std::span:不拥有数据的“视图”

写过 C 的朋友对指针和数组长度一定不陌生。传一个数组给函数,通常要同时传长度,一不小心就传错了。C++20 的 std::span 就是来解决这个问题的。

std::span 本质上是一个轻量级的“视图”,它不拥有数据,只保存指针和长度。它可以引用连续内存区域,比如数组、std::vectorstd::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 的线程

多线程编程中,最烦人的事情之一就是忘记 joindetach。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;
}

这个例子展示了几个关键点:

  • 日期字面量2024yDecember25d,读起来像自然语言
  • 日期计算:可以直接相减得到天数
  • 时区支持zoned_time 自动处理时区偏移
实际应用:我在做一个跨时区的日志系统时,用这个库轻松实现了 UTC 存储、本地显示。以前得手动处理夏令时,现在库全包了。

日历库还支持很多操作:

  • 获取星期几year_month_weekday 可以算出某个月的第几个星期几
  • 最后一天last 表示某月的最后一天,比如 2024y / February / last
  • 时区列表get_tzdb().zones 可以列出所有可用时区
注意:时区数据库需要系统支持。Linux 上通常没问题,Windows 上可能需要额外配置。我曾在 Windows 上遇到时区数据加载失败的问题,后来发现是缺少 IANA 时区文件。

说实话,这个库刚出来时我还有点不习惯。毕竟以前用 time_tstruct tm 习惯了。但用了几次之后,就再也回不去了。类型安全、表达力强,这才是现代 C++ 该有的样子。

好了,这四个特性就聊到这里。它们虽然不像 Concepts 或 Ranges 那样“炸裂”,但每一个都解决了实际开发中的痛点。尤其是 std::formatstd::jthread,我几乎每天都在用。如果你还没试过,强烈建议上手体验一下。


公众号:蓝海资料掘金营,微信deep3321