跳转至

14-C++23 新特性:标准库更好用与语法补强(入门)

目录页:见 C++ 小白教程目录

目标

  • 了解 C++23 相对 C++20 的“高频提升点”在哪里
  • 会用 std::expected 表达“可能失败但不想抛异常”的返回值
  • 会用 std::ranges::to 让 Ranges 的结果更容易落地成容器

先决知识(建议)

  • C++20 的 Ranges 基本用法(见 13 章 2)
  • std::optional / std::variant 的直觉(你可以先把它们当作“可空值 / 多选一”)

0. 先给结论:C++23 的关键词是“补齐与打磨”

和 C++20 那种“大功能上场”不同,C++23 更像是:

  • 标准库补齐:让很多“日常就想要”的工具变成标配
  • 语法打磨:零碎但顺手,减少样板代码
  • Ranges 继续增强:新增 view 让流水线更好用

现实里你最可能最先用到的是:expectedranges::tostring::contains、一些新的 views(比如 zip / enumerate)。


1. std::expected:把“错误”变成返回值的一部分

  • 专业名称:std::expected<T, E>(expected)
  • 类比:快递单:要么拿到包裹(T),要么拿到“失败原因”(E)
  • 作用:不靠异常也能表达失败,并且强迫调用方处理
  • 规则/坑点:它不是万能;对于“真正不可恢复”的错误,异常仍然有价值

1.1 基本用法:要么值,要么错误

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

std::expected<int, std::string> parsePositiveInt(const std::string& s) {
    if (s.empty()) return std::unexpected("empty");
    int x = 0;
    for (char c : s) {
        if (c < '0' || c > '9') return std::unexpected("not a number");
        x = x * 10 + (c - '0');
    }
    if (x <= 0) return std::unexpected("not positive");
    return x;
}

int main() {
    auto r = parsePositiveInt("123");
    if (!r) {
        std::cout << "error=" << r.error() << '\n';
        return 0;
    }
    std::cout << "value=" << *r << '\n';
    return 0;
}

直觉对比:

  • T:代表“这里通常会成功”
  • std::optional<T>:代表“可能没有值,但不关心原因”
  • std::expected<T, E>:代表“可能失败,而且你需要原因”

1.2 什么时候用 expected,什么时候用异常

更实用的判断方式是看“失败是不是业务的一部分”:

  • 更适合 expected:解析失败、用户输入不合法、文件不存在、网络超时(调用方通常能处理)
  • 更适合异常:违反前置条件、逻辑不可能发生、资源耗尽等“不可恢复/不该吞掉”的错误

2. std::ranges::to:把 view 的结果一键收集成容器

  • 专业名称:std::ranges::to<Container>(Ranges 收集器)
  • 类比:流水线上最后加一个“打包机”,把流里的东西装进箱子里
  • 作用:解决 C++20 Ranges 常见痛点:结果想变回 vector 很麻烦
  • 规则/坑点:这是 C++23;如果你用的是 C++20,需要手动 for 或用第三方库
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{1, 2, 3, 4, 5, 6};

    auto out = v
        | std::views::filter([](int x) { return x % 2 == 0; })
        | std::views::transform([](int x) { return x * x; })
        | std::ranges::to<std::vector<int>>();

    for (int x : out) std::cout << "x=" << x << '\n';
    return 0;
}

3. std::print:更顺手的输出(搭配 format)

  • 专业名称:std::print/std::println
  • 类比:把“格式化 + 输出”打包成一个动作
  • 作用:少写 << 拼接,输出更直观
  • 规则/坑点:依赖实现对 <print> 的支持情况;如果编译器/标准库较旧,可能不可用
#include <print>

int main() {
    std::println("sum={} {}", 2 + 3, "ok");
    return 0;
}

如果你的环境还没支持 <print>,你可以继续用 std::cout,或者在支持良好的环境下再切换。


4. string::contains:判断子串更直觉

#include <iostream>
#include <string>

int main() {
    std::string s = "hello world";
    std::cout << "has_hello=" << s.contains("hello") << '\n';
    return 0;
}

5. 新的 Ranges views:zip / enumerate(C++23)

很多人学 Ranges 最大的痛点是“能 filter/transform,但组合不够丰富”。C++23 增加了一批更常用的 view。

5.1 views::enumerate:遍历时同时拿下标与元素

#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    for (auto [i, x] : std::views::enumerate(v)) {
        std::cout << "i=" << i << " x=" << x << '\n';
    }
    return 0;
}

5.2 views::zip:并行遍历两个序列

#include <iostream>
#include <ranges>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> names{"alice", "bob"};
    std::vector<int> scores{90, 80};

    for (auto [name, score] : std::views::zip(names, scores)) {
        std::cout << "name=" << name << " score=" << score << '\n';
    }
    return 0;
}

规则/坑点(先记结论就行):

  • zip 会按“最短序列”截断
  • enumerate 里的下标类型是某种无符号整数类型(用它做减法时要小心)

6. C++23 的语法补强(挑最常见的讲)

6.1 if consteval:区分“编译期路径”和“运行期路径”

#include <iostream>

consteval int f_compiletime(int x) { return x + 1; }

constexpr int f(int x) {
    if consteval {
        return f_compiletime(x);
    } else {
        return x + 1;
    }
}

int main() {
    constexpr int a = f(1);
    std::cout << "a=" << a << '\n';
    return 0;
}

你可以把它理解成:在 constexpr 函数里,某些逻辑你只想在编译期做(比如预计算、校验),运行期就走普通路径。

6.2 Deducing this(你会在写链式 API/值类别转发时遇到)

它能让成员函数“根据对象是左值/右值”做不同返回,或者更准确地转发。学习阶段可以先知道它是“高级用法”,等你写库/写通用组件再深入。


7. 其他值得知道的标准库小工具(C++23)

这一节的目标是:让你看到“C++23 不只是 expected/print”,标准库也补了不少顺手的小能力。

7.1 std::to_underlying:从 enum class 拿到底层整数

#include <cstdint>
#include <iostream>
#include <utility>

enum class Color : std::uint8_t { red = 1, green = 2 };

int main() {
    std::cout << "red=" << std::to_underlying(Color::red) << '\n';
    return 0;
}

7.2 std::byteswap:字节序翻转(做协议/二进制时常用)

#include <bit>
#include <cstdint>
#include <iostream>

int main() {
    std::uint32_t x = 0x11223344u;
    std::uint32_t y = std::byteswap(x);
    std::cout << "y=" << y << '\n';
    return 0;
}

7.3 std::stacktrace:打印调用栈(支持情况依赖环境)

#include <iostream>
#include <stacktrace>

int main() {
    std::cout << std::stacktrace::current() << '\n';
    return 0;
}

如果你的编译器/标准库还没实现 <stacktrace>,这段会编译失败,这不是你写错,是工具链支持度问题。


8. 这一章你该怎么用(现实建议)

  • 学习与竞赛/刷题:优先把 C++20 的 Ranges/Concepts 学会;C++23 的 expected 很值得补
  • 工程与项目:先看你编译器与标准库版本是否“真的到位”,再决定能不能用 <print><expected>

常见坑

  • 错误:把 expected 当成“异常替代品”并滥用 → 结果:到处传播错误码,噪音大 → 正确:只在“可预期失败”的边界用它(解析/IO/校验)
  • 错误:不检查 expected 直接 *r → 结果:未定义行为/崩溃 → 正确:先判断 if (!r) 再取值
  • 错误:看到 C++23 就默认环境支持 → 结果:编译失败 → 正确:先确认工具链与 -std=c++23(或对应开关)

小练习

练习 1:expected 的错误信息

  • 输入/输出:读入一行字符串,输出解析到的整数;失败则输出原因
  • 约束:返回类型必须是 std::expected<int, std::string>
  • 提示:可以参考 1.1 的 parsePositiveInt
  • 目标:训练“先判断再取值”的习惯

练习 2:ranges::to 收集

  • 输入/输出:读入 N 个整数,输出所有奇数的绝对值(按原顺序)
  • 约束:必须用 view 管道,最后用 std::ranges::to<std::vector<int>>
  • 提示:绝对值可以写成一个 transform
  • 目标:把“流水线”与“落地”连起来

小结

  • C++23 里最“日常有感”的提升主要来自标准库:expectedranges::toprint、新的 views 等
  • 是否立刻用 C++23,取决于你的编译器与标准库版本成熟度

上一篇:见 13-C++20 新特性:Concepts、Ranges、Coroutines、Modules(入门) 下一篇:见 15-C++11 到 C++23:标准演进路线图(你该学什么)