C++23 新特性

在 C++20 搭好范围库与字符串格式化的大框架之后,C++23 标准重点围绕日常开发体验做了一次实用补齐。

对于初学的小伙伴来说,现阶段先把精力放在前面的 C++17 和 C++20 主线语法上即可。C++23 新增的工具都很直观,我们先了解它们的常见用法,以后阅读新项目代码时能够顺利看懂就可以了。

C++23 新特性概览

在 C++23 中,标准库补上了直接格式化打印、带错误原因的返回值包装以及视图转容器等省事工具,同时也简化了编译期分支与成员函数写法。常用的代表性特性如下表所示。

C++23 常用特性
代表特性 所属类别 / 头文件 主要变化与作用
std::print() / std::println() <print> 将 {} 占位符格式化与控制台输出合二为一,省去手写 std::cout <<
std::expected <expected> 在返回值中同时表达 “正常结果 T” 或 “错误原因 E”,替代错误码约定
std::ranges::to() <ranges> 将管道过滤或转换后的范围视图直接收集为 vector 等真实容器
std::mdspan <mdspan> 提供多维连续数组的轻量观察视图,适合矩阵与科学计算场景
if consteval 语言特性 专门判断当前代码是否处于编译期常量求值环境中
显式对象参数(Deducing this) 语言特性 支持在成员函数形参列表中通过 this 显式声明调用对象本身

直接格式化输出:std::print()

在 C++20 中,我们可以使用 std::format() 函数配合花括号({})占位符拼接字符串,但拼接完之后往往还得再写一遍 std::cout << 才能显示到屏幕上。

到了 C++23,标准库在 <print> 头文件中新增了 std::print() 和 std::println() 两个函数,把 “文本格式化” 与 “控制台输出” 合并成了一步。

语法:

#include <print>

std::print("格式字符串", 参数1, 参数2, ...);
std::println("格式字符串", 参数1, 参数2, ...);

说明:

  • std::print():按照 {} 占位符将格式化后的文本直接输出到控制台,末尾不会自动换行。
  • std::println():在完成格式化输出后,自动在末尾追加一个换行符,相当于自带了 '\n'。

示例 1:直接格式化输出文本

#include <print>
#include <string>

int main() {
    std::string name = "Jack";
    int score = 95;

    // 格式化输出并自动换行
    std::println("姓名:{},成绩:{}", name, score);

    // 末尾不自动换行
    std::print("正在处理...");
    std::println("完成");
    return 0;
}

运行结果如下。

姓名:Jack,成绩:95
正在处理...完成

分析:

在这个例子中,我们调用 std::println() 直接把 name 和 score 按格式打印到了屏幕上,省去了先生成临时字符串再交给 std::cout 的步骤。下方的 std::print("正在处理..."); 输出后不换行,紧跟着打印的 "完成" 就会接在同一行末尾。

带原因的错误处理:std::expected

当我们编写整数除法或数据解析这类可能失败的函数时,如果使用 std::optional,返回空状态 std::nullopt 只能表示 “操作失败”,调用方无法得知具体的失败原因。

为了补上这块短板,C++23 在 <expected> 头文件中引入了 std::expected<T, E> 模板类型,用来同时承载正常结果或错误信息。

语法:

#include <expected>

std::expected<正常结果类型, 错误信息类型>

说明:

std::expected<T, E> 就像一个二选一的结果包装盒,每次返回时只会处于以下 2 种状态之一:

  • 操作成功:内部保存着类型为 T 的正常结果,我们可以通过 .value() 或解引用运算符(*)取出结果。
  • 操作失败:通过 std::unexpected(错误信息) 返回类型为 E 的错误对象,调用方可以通过 .error() 读取具体的失败原因。

示例 2:返回计算结果或错误原因

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

// 成功返回商,失败返回错误原因
std::expected<int, std::string> safeDivide(int a, int b) {
    if (b == 0) {
        return std::unexpected("除数不能为 0");
    }
    return a / b;
}

int main() {
    auto res1 = safeDivide(20, 4);
    if (res1.has_value()) {
        std::cout << "计算成功:" << res1.value() << '\n';
    }

    auto res2 = safeDivide(20, 0);
    if (!res2.has_value()) {
        std::cout << "计算失败:" << res2.error() << '\n';
    }
    return 0;
}

运行结果如下。

计算成功:5
计算失败:除数不能为 0

分析:

在上面例子中,safeDivide() 的返回类型为 std::expected<int, std::string>。当我们传入 20 和 4 时,函数正常返回整数 5。当我们传入 20 和 0 时,函数通过 std::unexpected("除数不能为 0") 包装错误文本,外层调用 res2.error() 就能直接拿到失败原因。

在实际开发中,这两者的选型标准很清晰:如果失败原因只有一种,比如 “没查到记录”,使用 std::optional<T> 就够用了。如果需要把具体的错误原因传回给调用方,同时又不想抛出异常,那么使用 std::expected<T, E> 更合适。

视图转容器:std::ranges::to()

在 C++20 中,当我们通过范围库的 std::views::filter 或 std::views::transform 处理数据时,得到的是按需计算的 “视图”,而不是真正的容器。如果想把处理后的结果保存下来反复使用,以前还得手动创建空容器再逐个拷贝。

到了 C++23,<ranges> 头文件新增了 std::ranges::to<容器>() 工具,支持在管道末尾直接把筛选、转换后的结果收集为 std::vector 等真实容器。请看下面例子。

示例 3:将视图直接收集为 vector

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

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

    // 筛选偶数翻倍后直接转为 vector
    auto evenDoubled = nums
        | std::views::filter([](int n) { return n % 2 == 0; })
        | std::views::transform([](int n) { return n * 2; })
        | std::ranges::to<std::vector>();

    std::cout << "偶数的 2 倍:";
    for (int n : evenDoubled) {
        std::cout << n << ' ';
    }
    std::cout << '\n';
    return 0;
}

运行结果如下。

偶数的 2 倍:4 8 12

分析:

在这个例子中,整条竖线管道按照从左往右的顺序依次处理数据:先从 nums 中通过 views::filter 挑出偶数 2, 4, 6,再通过 views::transform 将每个偶数乘以 2 得到 4, 8, 12。

最后一步的 std::ranges::to<std::vector>() 会自动遍历前面的结果,并根据元素类型推导出 std::vector<int>,注意末尾的一对空圆括号 () 不能省略。这样我们连 <int> 都不用手动书写,就能一步拿到完整的容器对象 evenDoubled。

if consteval 与显式对象参数

除了标准库工具,C++23 还在编译期求值与类成员函数写法上补充了两项实用的语法改进。

1. 编译期求值分支(if consteval)

在 C++ 中,使用 constexpr 修饰的函数既可以在编译阶段求值,也可以在程序运行时像普通函数一样被调用。如果我们希望函数在这两种情况下执行不同的代码逻辑,就可以使用 C++23 新增的 if consteval 分支。

示例 4:区分编译期与运行期调用

#include <iostream>

constexpr int compute(int n) {
    if consteval {
        return n * 10;               // 编译期求值分支
    } else {
        return n * 20;               // 运行期调用分支
    }
}

int main() {
    constexpr int a = compute(5);    // 触发编译期求值
    int x = 5;
    int b = compute(x);              // 触发运行期调用

    std::cout << "编译期结果:" << a << '\n';
    std::cout << "运行期结果:" << b << '\n';
    return 0;
}

运行结果如下。

编译期结果:50
运行期结果:100

分析:

constexpr int a = compute(5); 要求在编译阶段算出常量结果,因此命中 if consteval 分支,返回 50。而 int b = compute(x); 传入的是普通运行期变量 x,因此走 else 分支,返回 100。

小伙伴们要注意它与 C++17 if constexpr 的写法区别:if consteval 后面不需要写圆括号条件,直接紧跟花括号 {} 即可。

2. 显式对象参数(Deducing this)

在传统的类成员函数中,this 指针是隐式存在的。但在编写返回成员引用的函数时,为了让普通对象拿到可修改引用、让 const 只读对象拿到只读引用,以前我们往往得把逻辑相同的函数重复写两遍。

在 C++23 中,我们可以在成员函数的第 1 个形参位置加上 this 关键字,把调用该函数的对象本身显式声明为引用参数。配合 auto& 自动推导后,写一份函数就能同时适配普通对象与 const 对象。下面我们来看一个例子。

示例 5:显式声明调用对象参数

#include <iostream>
#include <string>

struct User {
    std::string name;

    // 显式声明调用对象 self
    auto& getName(this auto& self) {
        return self.name;
    }
};

int main() {
    User u1 = {"Jack"};
    std::string& ref1 = u1.getName();          // 返回可修改引用
    ref1 = "Lucy";

    const User u2 = {"Tom"};
    const std::string& ref2 = u2.getName();    // 返回只读引用

    std::cout << "u1 姓名:" << u1.name << '\n';
    std::cout << "u2 姓名:" << ref2 << '\n';
    return 0;
}

运行结果如下。

u1 姓名:Lucy
u2 姓名:Tom

分析:

在这个例子中,auto& getName(this auto& self) 把调用对象显式声明为第 1 个引用形参 self。注意这里的 self 是对象引用而不是指针,因此函数体内直接通过点号写 self.name(不用写 this->name),外部调用时依然正常写 u1.getName()。

当普通对象 u1 调用时,self 自动推导为 User&,返回的 ref1 可以直接修改成员。当只读对象 u2 调用时,self 则推导为 const User&,返回安全的只读引用 ref2。对于这个例子来说,下面 2 种方式是等价的。

// 方式 1(推荐)
struct User {
    std::string name;

    auto& getName(this auto& self) {
        return self.name;
    }
};

// 方式 2
struct User {
    std::string name;

    std::string& getName() {
        return name;
    }

    const std::string& getName() const {
        return name;
    }
};

对比可以看出,以前的方式 2 必须分别编写普通成员函数和 const 成员函数两个重载版本,而使用 C++23 的方式 1 一份代码就兼顾了两种场景。

提示 如果小伙伴们想在本地编译运行 C++23 的新特性,建议使用 GCC 14、Clang 18 或 MSVC 2022(17.6 及以上)编译器,并在编译命令中加上 -std=c++23 参数。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号