C++23 新特性全面解析:从 deducing this 到 std::expected

大家好,我是林夕07。C++23 终于正式发布了,作为继 C++20 之后的又一个重要版本,它带来了一批非常实用的新特性。今天这篇文章,我挑了 6 个最值得关注的特性,带你逐一过一遍——不是泛泛而谈,而是每个特性都有完整代码,跑一遍你就懂了。


写在前面

如果你还在用 C++17 写代码,或者刚开始玩 C++20 的 concepts 和 ranges,那么 C++23 的这些新特性会让你觉得:“嘿,这帮人确实认真在改善日常开发体验。”

C++23 不是一个大刀阔斧的革命性版本,它更像是一次精心打磨——把开发者日常遇到的痛点一个个解决掉。格式化输出、错误处理、多维数组、模块化、协程……每一个都直击要害。

让我们开始吧。


1. deducing this(显式对象参数)

是什么

deducing this 是 C++23 引入的一个语法特性,允许成员函数的第一个参数显式声明对象的类型和值类别(左值/右值、const/non-const)。简单说,你可以用一个参数来代替隐式的 this

为什么需要

在 C++23 之前,如果你想让一个成员函数同时支持 const 和非 const 对象调用,通常要写两个重载:

// C++20 及之前的痛苦写法
class Widget {
    std::string name_;
public:
    std::string& get_name() { return name_; }
    const std::string& get_name() const { return name_; }
    // 如果有右值版本……再来一个
};

三个函数做同一件事,代码膨胀不说,维护起来更是噩梦。deducing this 让你用一个函数搞定所有情况。

代码示例

#include <iostream>
#include <string>

class Widget {
    std::string name_;
public:
    // 一个函数搞定所有值类别
    template <typename Self>
    auto&& get_name(this Self&& self) {
        return std::forward<Self>(self).name_;
    }

    // 链式调用:左值版本返回引用,右值版本返回移动后的值
    template <typename Self>
    auto&& set_name(this Self&& self, std::string new_name) {
        self.name_ = std::move(new_name);
        return std::forward<Self>(self);
    }
};

int main() {
    Widget w;
    w.set_name("Hello").set_name("World");  // 左值链式调用

    const Widget cw = []{
        Widget tmp;
        tmp.set_name("Const");
        return tmp;
    }();

    // cw.get_name() 也能正常调用,返回 const 引用
    std::cout << cw.get_name() << std::endl;  // 输出: Const
    std::cout << w.get_name() << std::endl;   // 输出: World
}

注意事项

  • this 作为显式参数不占运行时开销,编译器会将其优化掉
  • 这个特性对CRTP 模式的简化尤其显著——以前用 CRTP 需要大量模板代码,现在干净多了
  • 目前主流编译器支持情况:GCC 14+、Clang 18+、MSVC 19.38+

2. std::expected(错误处理新范式)

是什么

std::expected<T, E> 是一个表示"要么成功拿到值,要么失败拿到错误"的类型。它和 std::optional 类似,但额外携带了一个错误信息。

为什么需要

C++ 的错误处理一直是个争议话题。异常(exception)有性能开销,错误码(error code)容易被忽略,std::optional 又没法告诉你"为什么失败了"。std::expected 提供了一种零开销、不会被忽略、还能携带错误信息的方案。

如果你用过 Rust 的 Result<T, E>std::expected 的思路几乎一样。

代码示例

#include <iostream>
#include <expected>
#include <string>
#include <cmath>

// 自定义错误类型
enum class ParseError {
    EmptyInput,
    NotANumber,
    OutOfRange
};

std::expected<double, ParseError> parse_number(const std::string& input) {
    if (input.empty()) {
        return std::unexpected(ParseError::EmptyInput);
    }

    try {
        size_t pos = 0;
        double value = std::stod(input, &pos);

        if (pos != input.size()) {
            return std::unexpected(ParseError::NotANumber);
        }
        if (std::abs(value) > 1e9) {
            return std::unexpected(ParseError::OutOfRange);
        }

        return value;
    } catch (const std::exception&) {
        return std::unexpected(ParseError::NotANumber);
    }
}

int main() {
    auto result = parse_number("3.14");

    if (result) {
        std::cout << "解析成功: " << *result << std::endl;
    } else {
        switch (result.error()) {
            case ParseError::EmptyInput:
                std::cerr << "错误: 输入为空" << std::endl;
                break;
            case ParseError::NotANumber:
                std::cerr << "错误: 不是有效数字" << std::endl;
                break;
            case ParseError::OutOfRange:
                std::cerr << "错误: 数值超出范围" << std::endl;
                break;
        }
    }

    // 链式操作(类似 std::optional 的 transform/and_then)
    auto result2 = parse_number("42.0")
        .transform([](double v) { return v * 2; })
        .transform([](double v) { return "结果: " + std::to_string(v); });

    if (result2) {
        std::cout << *result2 << std::endl;  // 输出: 结果: 84.000000
    }
}

注意事项

  • std::expected 不使用异常,错误通过返回值传播,性能可预测
  • result.value() 在错误状态下调用会抛 std::bad_expected_access 异常,和 std::optional.value() 类似
  • 适合用在错误是正常控制流一部分的场景(如解析输入、IO操作),不适合用于真正的"异常"情况
  • transformand_then 链式操作让错误处理代码非常简洁

3. std::print / std::println(告别 printf 和 cout)

是什么

std::printstd::println 是 C++23 新增的格式化输出函数,底层基于 std::format,但使用更方便。printlnprint 的基础上自动添加换行符。

为什么需要

说实话,C++ 的格式化输出一直是"能用但不好用"的状态:

  • printf:类型不安全,C 风格,格式化字符串和参数对不上就炸
  • cout:类型安全但啰嗦,格式化能力弱,拼接字符串巨麻烦
  • std::format(C++20):很好,但没有直接输出到 stdout 的便捷函数

std::print 就是填补这个缺口的——兼具 printf 的简洁和 std::format 的类型安全。

代码示例

#include <print>
#include <string>
#include <vector>

int main() {
    // 基础用法——比 cout 简洁太多
    std::println("Hello, {}! 你今年 {} 岁了。", "林夕", 25);

    // 格式化对齐和精度
    double pi = 3.14159265358979;
    std::println("π = {:.4f}", pi);
    std::println("右对齐: [{:>20}]", "right");
    std::println("填充零: [{:0>8}]", 42);

    // 打印容器——以前要自己写循环或用 ranges
    std::vector<int> nums = {1, 2, 3, 4, 5};
    std::println("数组: {}", nums);  // 输出: 数组: [1, 2, 3, 4, 5]

    // 自定义类型也支持(需要特化 formatter)
    std::println("调试信息: x={}, y={}, status={}", 10, 20, "OK");

    // 输出到 stderr(错误信息)
    std::println(stderr, "这是一条错误日志");
}

注意事项

  • 编译器支持:GCC 13+、Clang 17+、MSVC 19.36+(需要 <print> 头文件)
  • 格式化语法和 std::format 完全一致,参考 fmtlib 文档即可
  • 性能比 cout 快得多——std::print 的输出速度通常接近 printf,且不涉及 iostream 的同步开销
  • 如果编译器暂不支持,可以用 fmtlib 库作为替代

4. std::mdspan(多维数组视图)

是什么

std::mdspan<T, Extents> 是一个轻量级的多维数组视图(类似 std::span 之于一维数组)。它不拥有数据,只是提供一个"多维索引访问"的接口,底层数据可以是连续内存、分块内存、甚至自定义布局。

为什么需要

科学计算、图像处理、机器学习……这些领域大量使用多维数组。C++ 以前处理二维以上数组的方式五花八门:二维指针、一维模拟多维、嵌套 std::vector……每种都有各自的坑。

std::mdspan 给了你一个标准的、零开销的、可自定义布局的多维数组接口。

代码示例

#include <iostream>
#include <print>
#include <mdspan>
#include <vector>
#include <numeric>

int main() {
    // 用一维 vector 作为底层存储
    std::vector<double> data(12);  // 3x4 矩阵
    std::iota(data.begin(), data.end(), 1.0);

    // 创建 3x4 的 mdspan 视图
    std::mdspan<double, std::dextents<size_t, 2>> matrix(data.data(), 3, 4);

    // 像真正的多维数组一样访问
    std::println("矩阵内容:");
    for (size_t i = 0; i < matrix.extent(0); ++i) {
        for (size_t j = 0; j < matrix.extent(1); ++j) {
            std::print("{:6.1f}", matrix[i, j]);
        }
        std::println();
    }
    // 输出:
    //   1.0   2.0   3.0   4.0
    //   5.0   6.0   7.0   8.0
    //   9.0  10.0  11.0  12.0

    // 遍历所有元素
    double sum = 0;
    for (auto& val : matrix) {
        sum += val;
    }
    std::println("总和: {}", sum);  // 输出: 总和: 78

    // 静态维度(编译期确定大小)
    std::mdspan<int, std::extents<size_t, 2, 3>> static_mat;
    // static_mat 的维度在编译期就是固定的,编译器可以做更多优化
}

注意事项

  • mdspan视图,不管理生命周期——底层数据必须保证有效
  • 支持自定义布局策略(layout),如行优先、列优先、分块布局等
  • std::mdspan 的维度可以是静态(编译期)或动态(运行时),静态维度有更多优化空间
  • 这个特性对数值计算库的作者来说是重大利好——终于有了统一的多维数组接口标准

5. import std;(标准库模块化)

是什么

import std; 是 C++23 引入的标准库模块化方式——一条 import 语句就可以使用整个标准库,替代几十行的 #include 头文件。

为什么需要

传统 #include 的问题说多了都是泪:

  • 编译慢:每个头文件都会引入大量代码,编译器要反复解析
  • 名称污染using namespace std; 会引入成千上万个符号
  • 隐式依赖:代码里用了 std::string,但不知道它依赖了哪些头文件

模块化从根本上解决了这些问题。import std; 是最激进的一步——一个模块包含所有标准库内容

代码示例

// 以前的写法——几十行 include
// #include <iostream>
// #include <string>
// #include <vector>
// #include <algorithm>
// #include <numeric>
// #include <format>
// ... 还要继续加

// C++23:一行搞定
import std;

int main() {
    std::vector<int> nums = {5, 3, 8, 1, 9, 2, 7};

    std::ranges::sort(nums);

    auto evens = nums
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * n; });

    std::println("排序后: {}", nums);
    std::println("偶数的平方: {}", std::vector<int>(evens.begin(), evens.end()));
}

编译命令(以 GCC 为例):

# 先编译标准库模块(只需一次)
g++ -std=c++23 -fmodules-ts -c std.cppm -o std.gcm

# 再编译你的代码
g++ -std=c++23 -fmodules-ts main.cpp -o main

MSVC 编译命令:

cl /std:c++23 /experimental:module main.cpp

注意事项

  • 目前各编译器的模块支持还在完善中,实际项目中建议渐进式采用
  • import std; 会引入标准库的所有内容,如果你追求编译速度的极致,可以用更细粒度的模块(如 import std.core;
  • 模块和 #include 可以混合使用,不需要一步到位
  • 这是 C++ 未来的大方向,尽早了解没有坏处

6. std::generator(协程生成器)

是什么

std::generator<T> 是 C++23 标准化的协程生成器类型,用于创建惰性求值的序列。你可以用 co_yield 产出值,用 co_await 暂停执行。

为什么需要

C++20 引入了协程的基础设施(co_awaitco_yieldco_return),但标准库没有提供现成的协程类型。开发者要么自己造轮子,要么用第三方库。

std::generator<T> 填补了这个空白——它是最常用的协程模式:生成一个值序列。无限序列、树遍历、状态机……都变得很优雅。

代码示例

#include <generator>
#include <iostream>
#include <print>
#include <ranges>

// 无限斐波那契数列——用传统方式几乎不可能优雅实现
std::generator<unsigned long long> fibonacci() {
    unsigned long long a = 0, b = 1;
    while (true) {
        co_yield a;          // 产出当前值
        auto temp = a + b;
        a = b;
        b = temp;
    }
}

// 生成器可以接受参数
std::generator<int> range(int start, int end, int step = 1) {
    for (int i = start; i < end; i += step) {
        co_yield i;
    }
}

int main() {
    // 取前 10 个斐波那契数
    std::println("斐波那契数列前 10 项:");
    int count = 0;
    for (auto num : fibonacci()) {
        std::print("{} ", num);
        if (++count >= 10) break;
    }
    std::println();
    // 输出: 0 1 1 2 3 5 8 13 21 34

    // 自定义 range
    std::println("自定义范围 (0, 20, 3):");
    for (auto n : range(0, 20, 3)) {
        std::print("{} ", n);
    }
    std::println();
    // 输出: 0 3 6 9 12 15 18

    // 和 ranges 结合使用
    auto fib_view = fibonacci()
        | std::views::take(10)
        | std::views::filter([](unsigned long long n) { return n % 2 == 0; });

    std::println("前 10 个斐波那契中的偶数:");
    for (auto n : fib_view) {
        std::print("{} ", n);
    }
    std::println();
    // 输出: 0 2 8 34
}

注意事项

  • std::generator惰性求值的——每次 co_yield 暂停,下次迭代才继续执行
  • 生成器在 for 循环结束后自动销毁,内部资源(如文件句柄)会在析构时释放
  • 无限序列是 generator 的杀手级用法——不需要提前定义"多大"
  • 目前仅 GCC 14+ 有较为完整的支持,其他编译器还在追赶中
  • std::generator 和 C++20 ranges 的结合使用非常自然

总结

特性 一句话总结 实用指数
deducing this 用一个函数替代 const/non-const 重载 ⭐⭐⭐⭐⭐
std::expected 类型安全的错误处理,告别异常和错误码的两难 ⭐⭐⭐⭐⭐
std::print C++ 终于有了好用的格式化输出 ⭐⭐⭐⭐⭐
std::mdspan 多维数组的标准化视图接口 ⭐⭐⭐⭐
import std; 一行代码引入整个标准库 ⭐⭐⭐⭐
std::generator 标准协程生成器,惰性序列的优雅解法 ⭐⭐⭐⭐

C++23 的这些特性,每一个都不是"炫技",而是真正解决日常开发中的痛点。我个人最推荐优先尝试的是 std::expectedstd::print——它们立竿见影,不需要改现有架构就能用起来。

如果你在用的编译器还不支持,别急。C++ 的新特性从来不是"一步到位"的,而是渐进式采纳。先了解,等工具链成熟了再上手不迟。

我是林夕07,如果这篇文章对你有帮助,欢迎点赞收藏。我们下一篇见!


编译环境参考:本文代码基于 GCC 14.1 / Clang 18 / MSVC 19.38 测试,使用 C++23 标准。部分特性在不同编译器上的支持程度可能有差异,建议查阅 cppreference.com 获取最新信息。

更多推荐