Bootstrap

全网最全最详细的C++23 标准详解:核心语言改进与新特性

1. 简介

C++23 是由 C++ 标准委员会最新发布的标准,旨在进一步提升 C++ 语言的功能和开发效率。作为一项重要的编程语言标准更新,C++23 引入了多个关键的新特性和改进,使开发者能够编写更高效、简洁和安全的代码。

与 C++20 相比,C++23 的变化虽然没有那么显著,但依然对语言的稳固性和可用性做出了许多重要改进。C++20 引入了大量新特性,如模块、协程、概念等,极大地丰富了 C++ 的语法和功能。而 C++23 则在这些基础上进行了补充和优化,解决了一些细节问题,并引入了新的编程工具和方法。

C++23 的新特性包括明确的对象参数(Deducing this)、if consteval、多维下标运算符、内建衰减复制支持、标记不可达代码(std::unreachable)、平台无关的假设([[assume]])、命名通用字符转义、扩展基于范围的 for 循环中临时变量的生命周期、constexpr 增强、简化的隐式移动、静态运算符 static operator[] 以及类模板参数推导(Class Template Argument Deduction from Inherited Constructors)。这些特性旨在提高代码的可读性和可维护性,同时优化编译器的性能和程序的执行效率。

此外,C++23 还对标准库进行了重要更新,增加了新的容器类型如 flat_mapflat_set,引入了多维视图(mdspan)以及标准生成器协程(Generator Coroutines),并改进了字符串格式化和错误处理机制(如 std::expected)。这些更新使得 C++ 在处理复杂数据结构和并发编程时更加得心应手。

总体而言,C++23 通过一系列细致入微的改进和新增功能,进一步巩固了 C++ 作为高效、强大编程语言的地位,为开发者提供了更丰富的工具箱,助力他们在各种应用场景中编写出色的代码,本文将详细介绍这些新特性,并通过代码示例帮助开发人员更好地理解和应用这些技术。

2. 新增语言特性

2.1. 明确的对象参数 (Deducing this)

C++23引入了明确的对象参数,允许在非静态成员函数中明确指定对象参数。这一特性简化了某些复杂的C++编程模式,如Curiously Recurring Template Pattern (CRTP)。

示例代码:

struct Base {
    template <typename T>
    void func(T&& t) {
        static_cast<T*>(this)->impl(std::forward<T>(t));
    }
};

struct Derived : Base {
    void impl(int x) {
        // Implementation for int
    }
};

int main() {
    Derived d;
    d.func(42);  // Calls Derived::impl(int)
}

示例解析:
在这个示例中,展示了如何使用明确的对象参数来简化 CRTP 模式。以下是详细解析:

  • 定义基类 Base
    • Base 类中定义了一个模板成员函数 func,它接受一个泛型参数 T
    • func 函数内部,使用 static_castthis 指针转换为 T* 类型,并调用 T 类的 impl 成员函数。
    • 这种方式允许在基类中定义通用的接口,而具体的实现则由派生类提供。
  • 定义派生类 Derived
    • Derived 类继承自 Base,并实现了一个 impl 成员函数,该函数接受一个 int 参数。
    • impl 函数是 Derived 类的具体实现,当基类的 func 调用时,将会转发给这个函数。
  • main 函数中使用
    • 创建一个 Derived 类的实例 d
    • 调用 d.func(42) 时,func 函数中的 static_cast 会将 this 指针转换为 Derived*,然后调用 Derived::impl 函数。
    • 这样,通过基类 Base 中定义的通用接口 func,实现了对派生类 Derived 中具体实现 impl 的调用。

这个示例展示了明确的对象参数如何简化 CRTP 模式,使得基类能够定义通用接口,而派生类提供具体实现。这种方式提高了代码的灵活性和可维护性,同时减少了模板代码的复杂性。C++23 的这一新特性,使得编写和使用 CRTP 模式更加直观和高效。

2.2. if consteval

if consteval 关键字用于在编译时判断一个常量表达式是否正在求值,从而增强了编译时计算的能力。通过 if consteval,开发者可以在编译时与运行时进行不同的代码路径选择,使得编译时计算更加灵活和强大。

示例代码:

constexpr int compute(int x) {
    if consteval {
        return x * 2;
    } else {
        return x * 3;
    }
}

int main() {
    constexpr int result = compute(5);  // Result is 10 at compile-time
}

示例解析:
在这个示例中,展示了如何使用 if consteval 关键字来判断一个常量表达式是否正在求值,并选择不同的代码路径。以下是详细解析:

  • 定义 compute 函数
    • compute 函数是一个 constexpr 函数,接受一个整数参数 x
    • 使用 if consteval 关键字在编译时判断是否在编译时进行计算。
    • 如果是编译时计算,返回 x * 2
    • 如果不是编译时计算,返回 x * 3
  • main 函数中调用 compute
    • 定义 constexpr int result = compute(5);,在编译时计算 compute(5) 的结果。
    • 由于 compute 函数在编译时进行计算,if consteval 条件为真,返回 5 * 2,结果为 10

通过这种方式,if consteval 关键字提供了一种灵活的机制,使得开发者可以在编译时与运行时进行不同的代码路径选择。这样不仅增强了编译时计算的能力,还可以根据实际情况优化代码路径,提高程序的性能和效率。这对于需要在编译时进行大量计算的场景,如常量表达式求值、编译时优化等,特别有用。

2.3. 多维下标运算符

多维下标运算符增强了对多维数组的支持,使代码更加直观和简洁。通过定义多维下标运算符,开发者可以更加方便地访问和操作多维数组数据,使代码逻辑更加清晰。

示例代码:

#include <iostream>
#include <array>

template <typename T, size_t Rows, size_t Cols>
struct Matrix {
    std::array<T, Rows * Cols> data;

    T& operator[](std::pair<size_t, size_t> idx) {
        return data[idx.first * Cols + idx.second];
    }

    const T& operator[](std::pair<size_t, size_t> idx) const {
        return data[idx.first * Cols + idx.second];
    }
};

int main() {
    Matrix<int, 3, 3> mat = { { 1, 2, 3, 4, 5, 6, 7, 8, 9 } };
    std::cout << mat[{1, 2}] << std::endl;  // Output: 6
}

示例解析:
在这个示例中,展示了如何使用多维下标运算符来增强对多维数组的支持。以下是详细解析:

  • 定义 Matrix 结构体模板
    • Matrix 结构体模板定义了一个通用的二维矩阵,使用 std::array 存储数据。模板参数 T 表示矩阵元素类型,RowsCols 表示矩阵的行数和列数。
    • data 成员变量是一个大小为 Rows * Colsstd::array,用于存储矩阵数据。
  • 定义多维下标运算符
    • operator[] 重载函数接受一个 std::pair<size_t, size_t> 类型的参数 idx,表示要访问的元素位置。
    • operator[] 函数内部,通过计算 idx.first * Cols + idx.second 得到一维数组中的索引,从而访问 data 中的相应元素。
    • 提供了两个版本的 operator[],一个用于非 const 对象,另一个用于 const 对象,确保在不同上下文中都可以正确访问矩阵数据。
  • main 函数中使用 Matrix
    • 创建一个 Matrix<int, 3, 3> 对象 mat,并初始化数据。
    • 通过多维下标运算符访问矩阵中的元素。例如,mat[{1, 2}] 访问矩阵第二行第三列的元素,输出结果为 6。

通过使用多维下标运算符,可以更加直观和简洁地访问和操作多维数组数据。这种方式不仅提高了代码的可读性,还减少了错误的可能性,使得开发者在处理复杂数据结构时更加得心应手。C++23 中的这一特性为开发者提供了更强大的工具,帮助他们编写高效、清晰的代码。

2.4. 内建衰减复制支持

C++23 改进了某些上下文中衰减复制的处理,提供了更可预测的行为。衰减复制(decay copy)是将参数类型转换为其基本类型的过程,这在模板编程和函数重载中非常有用。C++23 通过增强对衰减复制的支持,使得处理模板参数更加直观和可靠。

示例代码:

#include <type_traits>

template<typename T>
void process(T&& arg) {
    std::decay_t<T> val = std::forward<T>(arg);
    // val is now a decayed copy of arg
}

int main() {
    int x = 42;
    process(x);  // int
    process(42);  // int
    process("Hello");  // const char*
}

示例解析:
在这个示例中,展示了如何使用内建衰减复制支持来处理模板参数。以下是详细解析:

  • 模板函数 process 的定义
    • process 函数是一个接受通用引用参数 T&& arg 的模板函数。
    • 在函数内部,使用 std::decay_t<T>arg 进行衰减复制,并将结果存储在 val 变量中。std::decay_t 是一个类型转换工具,它将 T 转换为其基本类型,例如将数组类型转换为指针类型,将函数类型转换为指针类型,移除引用和 cv 限定符等。
    • std::forward<T>(arg) 用于完美转发 arg 参数,确保在转发时保持其左值或右值属性。
  • main 函数中调用 process
    • 调用 process(x),其中 x 是一个整型变量。x 被传递为 int& 类型,在 process 函数内部经过 std::decay_t 处理后,val 的类型为 int
    • 调用 process(42),其中 42 是一个整型字面值常量。42 被传递为 int&& 类型,在 process 函数内部经过 std::decay_t 处理后,val 的类型为 int
    • 调用 process("Hello"),其中 "Hello" 是一个字符串字面值常量。"Hello" 被传递为 const char(&)[6] 类型,在 process 函数内部经过 std::decay_t 处理后,val 的类型为 const char*

通过这种方式,C++23 中的内建衰减复制支持使得模板参数的处理更加直观和可靠。在模板函数中使用 std::decay_t 可以确保参数被正确地转换为其基本类型,从而避免类型转换错误和未定义行为。这一特性特别适用于泛型编程和库开发,使得代码更加健壮和易于维护。

2.5. 标记不可达代码 (std::unreachable)

新增的 std::unreachable 用于标记程序中不可达的代码部分,帮助编译器进行更激进的优化。通过明确标记不可达的代码,编译器可以进行更有效的优化和错误检查,减少不必要的警告或误报。

示例代码:

#include <utility>

[[noreturn]] void error() {
    throw "error";
}

int main() {
    int x = 0;
    if (x == 0) {
        error();
    }
    std::unreachable();  // Compiler knows this point is never reached
}

示例解析:
在这个示例中,展示了如何使用 std::unreachable 来标记不可达的代码部分。以下是详细解析:

  • 定义 error 函数
    • error 函数使用 [[noreturn]] 属性标记,表明该函数不会返回。它通过抛出异常来中断程序的正常执行流。
    • error 函数被调用时,程序会跳出当前执行路径,而不会继续执行后续代码。
  • main 函数中调用 error
    • main 函数中,定义一个整型变量 x 并初始化为 0
    • 使用 if 语句检查 x 的值。如果 x 等于 0,则调用 error 函数。
  • 使用 std::unreachable 标记不可达代码
    • error 函数调用后,程序的执行流被中断,后续代码将不会被执行。
    • std::unreachable() 用于明确告诉编译器,该位置的代码永远不会被执行。这样,编译器可以优化此部分代码,避免不必要的检查和警告。

示例输出解释
由于在 main 函数中调用了 error 函数,程序会在抛出异常后终止,std::unreachable() 所在的代码段永远不会被执行。因此,编译器知道该位置不可达,可以进行相应的优化。通过使用 std::unreachable,开发者可以帮助编译器更好地理解代码的逻辑结构,优化编译过程中的代码生成和警告检查。

2.6. 平台无关的假设 ([[assume]])

[[assume]] 属性允许开发者在代码中声明某些条件总是为真,帮助编译器进行更好的优化,同时提高代码的可移植性。通过使用 [[assume]],编译器可以利用这些假设进行更多的优化,例如消除不必要的分支和检查。

示例代码:

#include <cassert>

int divide(int a, int b) {
    [[assume(b != 0)]];
    return a / b;
}

int main() {
    int result = divide(10, 2);  // Compiler assumes b is not 0
    assert(result == 5);
}

示例解析:
在这个示例中,展示了如何使用 [[assume]] 属性来帮助编译器进行优化。以下是详细解析:

  • 定义 divide 函数
    • divide 函数接受两个整数参数 ab,并返回 a 除以 b 的结果。
    • 使用 [[assume(b != 0)]] 声明 b 永远不会等于 0。这意味着函数内部假设 b 不为 0,从而消除除以零的检查。
  • main 函数中调用 divide
    • main 函数中,调用 divide(10, 2) 并将结果存储在 result 变量中。
    • 由于 [[assume(b != 0)]] 声明 b 不为 0,编译器可以优化 divide 函数,省略除以零的检查。
  • 断言结果
    • 使用 assert(result == 5) 断言 result 的值为 5,以确保函数返回正确的结果。

示例输出解释
由于 [[assume(b != 0)]] 告诉编译器 b 永远不会为 0,编译器可以优化 divide 函数,去掉除以零的检查,从而生成更高效的代码。通过使用 [[assume]],开发者可以提供额外的信息给编译器,帮助其进行更激进的优化,提高程序的性能和可移植性。这对于需要严格性能优化的场景,例如嵌入式系统或高性能计算,非常有用。

2.7. 命名通用字符转义

C++23 支持命名的 Unicode 字符转义,增强了代码的可读性和国际化支持。这一特性允许开发者使用更加直观和描述性的方式来表示 Unicode 字符,从而提高代码的可读性,尤其是在处理多语言文本或特殊符号时。

示例代码:

#include <iostream>

int main() {
    char smiley = '\N{WHITE SMILING FACE}';
    std::cout << smiley << std::endl;
}

示例解析:
在这个示例中,展示了如何使用命名通用字符转义来表示 Unicode 字符。以下是详细解析:

  • 命名通用字符转义
    • '\N{WHITE SMILING FACE}' 是一个新的字符转义语法,用于表示 Unicode 字符。WHITE SMILING FACE 是 Unicode 字符的名字,对应的 Unicode 码点是 U+263A。
    • 这种命名方式相比直接使用 Unicode 码点更加直观,开发者无需记住具体的码点,只需使用字符的描述性名字即可。
  • main 函数中使用
    • 声明一个字符变量 smiley,并将其初始化为 '\N{WHITE SMILING FACE}'。此时,smiley 变量中存储的是对应的 Unicode 字符。
    • 使用 std::cout 输出 smiley 变量,程序将输出一个白色笑脸字符。

通过使用命名通用字符转义,开发者可以更加方便地在代码中使用 Unicode 字符,特别是在处理多语言文本、特殊符号或表情符号时。这不仅提高了代码的可读性,还增强了代码的国际化支持,使得编写和维护多语言应用程序更加容易。C++23 的这一特性为开发者提供了一个更加直观和描述性的方法来表示 Unicode 字符,减少了错误的可能性,并且使得代码更加易于理解和维护。

2.8. 扩展基于范围的 for 循环中临时变量的生命周期

C++23 扩展了基于范围的 for 循环初始化器中临时变量的生命周期,使得临时变量在循环体内保持有效。这一改进解决了之前版本中临时变量在每次循环迭代结束后即销毁的问题,确保临时变量在整个循环过程中都保持有效,增强了代码的稳定性和可读性。

示例代码:

#include <vector>
#include <iostream>

int main() {
    for (const auto& num : std::vector<int>{1, 2, 3, 4, 5}) {
        std::cout << num << ' ';
    }
    return 0;
}

示例解析:
在这个示例中,展示了如何利用 C++23 扩展的基于范围的 for 循环中临时变量的生命周期来进行更直观的数组遍历。以下是详细解析:

  • 临时变量的初始化
    • 在基于范围的 for 循环中,我们使用了一个临时变量 std::vector<int>{1, 2, 3, 4, 5} 来初始化循环。
    • 该临时变量是一个包含 5 个整数的 std::vector,在整个 for 循环中都保持有效。
  • 遍历临时变量
    • for (const auto& num : std::vector<int>{1, 2, 3, 4, 5}) 表达式中,num 是对临时变量 std::vector<int> 中每个元素的常量引用。
    • 在 C++23 之前的标准中,每次循环迭代结束时,临时变量会被销毁,然后在下一次迭代时重新创建。这可能导致性能开销和潜在的错误。
    • C++23 扩展了临时变量的生命周期,确保其在整个循环过程中保持有效。这样,临时变量只需创建一次,并在整个循环体内保持有效,从而提高了性能并确保了逻辑的一致性。
  • 输出结果
    • 在循环体内,通过 std::cout 输出 num 的值。
    • 由于临时变量在整个循环过程中保持有效,每次迭代都可以正确访问和输出 std::vector<int> 中的元素,结果输出为 1 2 3 4 5

通过这种方式,C++23 的这一改进提高了基于范围的 for 循环的效率和可靠性,使得代码更加简洁和稳定。开发者可以更安全地使用临时变量进行遍历操作,而无需担心其生命周期问题。

2.9. constexpr 增强

C++23 进一步放宽了 constexpr 的限制,使得更多的函数和表达式可以在编译时计算,包括 std::type_info::operator==std::bitsetstd::unique_ptr、部分 <cmath> 函数以及 std::to_charsstd::from_chars 的整数重载。

示例代码:

#include <bitset>
#include <memory>
#include <cmath>

constexpr std::bitset<8> bitset_op() {
    std::bitset<8> b;
    b.set(3);
    return b;
}

constexpr std::unique_ptr<int> unique_ptr_op() {
    auto ptr = std::make_unique<int>(42);
    return ptr;
}

constexpr double compute_sqrt(double x) {
    return std::sqrt(x);
}

int main() {
    constexpr auto b = bitset_op();
    static_assert(b[3] == true, "Bit 3 should be set");

    constexpr auto ptr = unique_ptr_op();
    // Note: Cannot dereference constexpr unique_ptr in compile-time context

    constexpr double result = compute_sqrt(9.0);
    static_assert(result == 3.0, "sqrt(9.0) should be 3.0");
}

示例解析:
在这个示例中,我们展示了 C++23 对 constexpr 的增强,通过允许更多标准库组件和数学函数在编译时计算来提高代码的效率和灵活性。以下是详细解析:

  • constexpr 操作 std::bitset
    • bitset_op 函数创建了一个 std::bitset<8> 对象,并设置了第 3 位为 1。
    • 返回的 std::bitset 对象是 constexpr,可以在编译时使用。
    • main 函数中,constexpr auto b = bitset_op() 初始化了一个 constexprstd::bitset 对象 b,并使用 static_assert 断言第 3 位为 1。这表明 std::bitset 的部分操作现在可以在 constexpr 上下文中进行。
  • constexpr 操作 std::unique_ptr
    • unique_ptr_op 函数创建了一个 std::unique_ptr<int>,并将其指向一个值为 42 的整数。
    • 返回的 std::unique_ptr 对象是 constexpr,可以在编译时使用。
    • main 函数中,constexpr auto ptr = unique_ptr_op() 初始化了一个 constexprstd::unique_ptr<int> 对象 ptr。虽然在编译时不能解引用 constexprstd::unique_ptr,但可以验证其构造和移动操作在编译时是有效的。
  • constexpr 操作 <cmath> 函数
    • compute_sqrt 函数使用了 std::sqrt 计算平方根,并被标记为 constexpr
    • main 函数中,constexpr double result = compute_sqrt(9.0) 初始化了一个 constexprdoubleresult,并使用 static_assert 断言其值为 3。这表明 <cmath> 中的部分数学函数现在可以在 constexpr 上下文中使用。

通过这些示例,展示了 C++23 对 constexpr 的增强使得更多标准库组件和数学函数可以在编译时计算,提高了代码的效率和安全性。这些改进使得开发者能够编写更高效、性能更优的代码,并利用编译时计算的优势进行更多优化。

2.10. 简化的隐式移动

C++23简化了对象在函数返回时的隐式移动,使得返回局部对象更加高效。在以前的标准中,返回局部对象可能会触发拷贝构造函数或移动构造函数,而C++23通过优化使得这种操作更加高效,减少了不必要的拷贝和移动操作,提高了程序性能。

示例代码:

#include <iostream>
#include <vector>

std::vector<int> create_vector() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    return vec;  // Implicit move occurs here
}

int main() {
    std::vector<int> vec = create_vector();
    for (int v : vec) {
        std::cout << v << ' ';
    }
    return 0;
}

示例解析:
在这个示例中,我们展示了如何利用C++23简化的隐式移动特性来提高函数返回局部对象的效率。以下是详细解析:

  • 定义函数 create_vector
    • create_vector 函数创建并初始化一个局部的 std::vector<int> 对象 vec,其中包含了一些整数。
    • 在函数末尾,vec 被返回。此时,触发了隐式移动操作。
  • 隐式移动操作
    • 在以前的标准中,返回局部对象可能会触发拷贝构造函数或移动构造函数,视具体情况而定。尽管移动构造函数已经比拷贝构造函数高效,但仍有改进的空间。
    • C++23 通过优化,使得返回局部对象时可以直接移动,不再进行不必要的拷贝或复杂的移动构造。这种优化确保了资源的高效传递,减少了额外的性能开销。
  • main 函数中使用 create_vector
    • 调用 create_vector 函数并将返回的 std::vector<int> 对象赋值给变量 vec
    • 由于 C++23 的优化,vec 的构造变得更加高效,减少了内存分配和数据拷贝的开销。
    • 使用范围for循环遍历 vec,并输出其中的每个元素。

通过这种方式,C++23的简化隐式移动特性显著提高了返回局部对象的效率,使代码更高效和简洁。这一特性对需要频繁返回大对象的函数特别有用,能够减少内存和性能开销,提高程序的整体性能。

2.11. 静态运算符 static operator[]

C++23 允许为类定义静态下标运算符,使得类可以像数组一样使用静态下标进行访问。通过这种方式,开发者可以更加方便地管理和操作静态数组,使代码更加直观和简洁。

示例代码:

#include <iostream>

class StaticArray {
public:
    static int data[10];

    static int& operator[](std::size_t index) {
        return data[index];
    }
};

int StaticArray::data[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

int main() {
    StaticArray::data[3] = 42;
    std::cout << StaticArray::data[3] << std::endl;  // Output: 42
    return 0;
}

示例解析:
在这个示例中,展示了如何使用静态下标运算符来访问和操作静态数组。以下是详细解析:

  • 定义类 StaticArray
    • StaticArray 类包含一个静态成员数组 data,用于存储 10 个整数。
    • 该数组在类外部初始化,初始值为 {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
  • 定义静态下标运算符 operator[]
    • operator[] 是一个静态成员函数,接受一个 std::size_t 类型的索引参数,并返回数组 data 中对应位置的元素引用。
    • 通过定义静态下标运算符,可以像访问普通数组一样访问和修改静态数组的元素。
  • main 函数中使用 StaticArray
    • 通过 StaticArray::data[3] = 42 语句,使用静态下标运算符访问并修改 data 数组的第四个元素,将其值设置为 42。
    • 使用 std::cout << StaticArray::data[3] << std::endl; 语句输出 data 数组的第四个元素,结果为 42。

通过这种方式,C++23 的静态下标运算符使得类的静态数组可以像普通数组一样方便地进行访问和操作。开发者可以利用这一特性编写更加直观和易于理解的代码,同时保持代码的简洁性和高效性。这一特性在需要频繁访问和修改静态数据的场景中尤为有用,提高了代码的可维护性和可读性。

2.12. 类模板参数推导 (Class Template Argument Deduction from Inherited Constructors)

C++23 允许从继承的构造函数中推导类模板参数,简化了模板类的使用。通过这一特性,开发者可以更方便地使用模板类,减少了显式指定模板参数的繁琐操作,提高了代码的简洁性和可维护性。

示例代码:

#include <iostream>

template <typename T>
struct Base {
    T value;
    Base(T val) : value(val) {}
};

struct Derived : Base<int> {
    using Base::Base;
};

int main() {
    Derived d(42);
    std::cout << d.value << std::endl;  // Output: 42
    return 0;
}

示例解析:
在这个示例中,展示了如何利用类模板参数推导从继承的构造函数中推导模板参数。以下是详细解析:

  • 定义模板类 Base
    • Base 是一个模板类,带有一个类型参数 T,包含一个成员变量 value
    • 构造函数 Base(T val) 接受一个 T 类型的参数并初始化成员变量 value
  • 定义继承类 Derived
    • Derived 类继承自 Base<int>,指定 Tint 类型。
    • 使用 using Base::Base; 语句继承 Base 的构造函数,使得 Derived 可以直接使用 Base 的构造函数进行初始化。
  • main 函数中使用 Derived
    • 创建一个 Derived 类的实例 d,并传递一个整数值 42 进行初始化。
    • 由于 Derived 继承了 Base 的构造函数,编译器能够从传递的参数 42 推导出 Base 的模板参数 Tint,并正确调用 Base 的构造函数进行初始化。
    • 使用 std::cout 输出 d.value 的值,结果为 42

通过这种方式,C++23 的类模板参数推导特性简化了模板类的使用,使得从继承的构造函数中推导模板参数成为可能。开发者可以利用这一特性编写更加简洁和易于维护的代码,减少了显式指定模板参数的复杂性,特别是在需要频繁创建模板类实例的场景中,提高了代码的可读性和灵活性。

3. 标准库增强

3.1. 字符串格式化改进

C++23 改进了字符串格式化功能,包括支持格式化整个范围的内容。新的格式化功能使得字符串处理更加直观和强大,简化了格式化操作,增强了代码的可读性和维护性。

示例代码:

#include <iostream>
#include <format>
#include <vector>

int main() {
    std::string name = "Alice";
    int age = 30;
    std::string output = std::format("Name: {}, Age: {}", name, age);
    std::cout << output << std::endl;

    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::string formatted_numbers = std::format("Numbers: {}", numbers);
    std::cout << formatted_numbers << std::endl; // C++23将会支持此特性
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 C++23 改进的字符串格式化功能来处理和格式化字符串。以下是详细解析:

  • 基本字符串格式化
    • 定义了两个变量:nameage,分别表示名字和年龄。
    • 使用 std::format 函数进行字符串格式化,将 nameage 的值插入到格式字符串 "Name: {}, Age: {}" 中。
    • 格式化后的字符串存储在 output 变量中,并通过 std::cout 输出,结果为 "Name: Alice, Age: 30"
  • 格式化整个范围的内容
    • 定义了一个 std::vector<int> 类型的变量 numbers,包含一组整数。
    • 使用 std::format 函数格式化整个范围的内容,将 numbers 的值插入到格式字符串 "Numbers: {}" 中。
    • 格式化后的字符串存储在 formatted_numbers 变量中,并通过 std::cout 输出。由于完整支持范围格式化是 C++23 的新特性,实际运行环境需要支持 C++23 标准,结果为 "Numbers: {1, 2, 3, 4, 5}"

通过这种方式,C++23 的字符串格式化改进使得处理和格式化字符串变得更加方便和高效。开发者可以利用这一新特性编写更加简洁和易于维护的代码,减少了手动拼接字符串的繁琐操作,并提高了代码的可读性和表达力。这对于需要处理大量字符串操作的应用场景,如日志记录、用户界面生成和数据报告等,特别有用。

3.2. 新容器:flat_mapflat_set

C++23 提供了性能优化的平面映射 (flat_map) 和集合 (flat_set) 容器,这些容器在某些特定场景下可以比传统的 std::mapstd::set 提供更高效的性能。这些容器使用连续的内存块存储元素,从而减少了内存分配和访问时间,提高了缓存命中率,非常适合于小型数据集或频繁查找操作的场景。

示例代码:

#include <flat_map>
#include <iostream>

int main() {
    std::flat_map<int, std::string> map = {{1, "one"}, {2, "two"}};
    map[3] = "three";
    for (const auto& [key, value] : map) {
        std::cout << key << ": " << value << '\n';
    }
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 flat_map 容器来存储和访问键值对。以下是详细解析:

  • 定义和初始化 flat_map
    • std::flat_map<int, std::string> 定义了一个平面映射容器 map,其键类型为 int,值类型为 std::string
    • 使用初始化列表 {{1, "one"}, {2, "two"}}map 进行初始化,存储了两个键值对。
  • 添加新的元素
    • 使用下标运算符 map[3] = "three" 添加新的键值对 3: "three"map 中。
    • 由于 flat_map 的底层实现是一个排序的连续内存块,因此插入操作可能涉及移动和排序元素,但整体效率通常高于传统的 std::map
  • 遍历和输出 flat_map
    • 使用基于范围的 for 循环遍历 map 中的所有键值对。
    • 通过解构绑定 const auto& [key, value] 获取每个键值对,并使用 std::cout 输出键和值。

通过这种方式,flat_map 容器提供了比传统 std::map 更高效的性能,特别适用于小型数据集或需要频繁查找操作的场景。开发者可以利用 flat_mapflat_set 容器来编写更加高效和简洁的代码,同时减少内存分配和访问时间,提高程序的整体性能。这些新容器在需要优化内存使用和提高缓存命中率的应用中尤为有用。

3.3. 多维视图 (mdspan)

mdspan 提供了处理多维数组的视图类型,对于科学计算和性能关键的应用非常重要。mdspan 是一种轻量级的多维数组视图,不持有数据,而是提供了对现有数据的多维访问方式。它结合了指针和多维索引的优点,使得数据访问更加高效和灵活。

示例代码:

#include <experimental/mdspan>
#include <iostream>

using namespace std::experimental;

int main() {
    int data[6] = {1, 2, 3, 4, 5, 6};
    mdspan<int, extents<3, 2>> matrix(data);

    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 2; ++j) {
            std::cout << matrix(i, j) << ' ';
        }
        std::cout << '\n';
    }
}

示例解析:
在这个示例中,我们展示了如何使用 mdspan 来创建一个多维数组视图,并访问其元素。以下是详细解析:

  • 定义数据数组
    • 创建一个包含 6 个整数的 data 数组,用于存储二维矩阵的数据。
  • 创建 mdspan 视图
    • 使用 mdspan<int, extents<3, 2>> 创建一个名为 matrix 的视图,表示一个 3 行 2 列的矩阵。
    • mdspan 使用指向 data 数组的指针,因此 matrix 不持有数据,而只是对现有数据的视图。
  • 遍历和访问元素
    • 使用嵌套的 for 循环遍历矩阵的行和列,通过 matrix(i, j) 访问矩阵的元素。
    • 输出矩阵的元素,结果为:
1 2 
3 4 
5 6 

通过这种方式,mdspan 提供了一个高效的多维数组视图,使得科学计算和性能关键的应用可以更方便地管理和操作多维数据。mdspan 不持有数据,而是通过指针和索引访问现有数据,减少了不必要的数据复制和内存分配,提高了访问效率和性能。这对于需要频繁操作多维数组的应用,如数值计算、图像处理和机器学习等,非常有用。

3.4. 标准生成器协程

C++23 对协程进行了进一步的完善和增强,引入了标准生成器协程。生成器协程使得创建惰性序列变得更加容易和高效,可以在协程内逐步产生值,并在每次调用时恢复执行。

示例代码:

#include <coroutine>
#include <iostream>

template <typename T>
struct Generator {
    struct promise_type {
        T value;
        std::suspend_always yield_value(T v) {
            value = v;
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        Generator get_return_object() {
            return Generator{ std::coroutine_handle<promise_type>::from_promise(*this) };
        }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;

    Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }

    bool next() {
        handle.resume();
        return !handle.done();
    }

    T getValue() { return handle.promise().value; }
};

Generator<int> sequence(int start, int step) {
    for (int i = start;; i += step) {
        co_yield i;
    }
}

int main() {
    auto gen = sequence(0, 5);
    for (int i = 0; i < 10; ++i) {
        gen.next();
        std::cout << gen.getValue() << ' ';
    }
    return 0;
}

示例解析:
在这个示例中,我们展示了如何使用标准生成器协程来创建一个生成器,该生成器可以按需生成一个整数序列。以下是详细解析:

  • 定义生成器结构体 Generator
    • Generator 是一个模板结构体,使用 T 作为生成的值的类型。
    • 内部定义了一个 promise_type 结构体,表示协程的承诺类型。
  • 定义 promise_type 结构体
    • promise_type 结构体包含了生成器协程的核心逻辑。
    • yield_value 方法用于将值 v 返回给调用方,并挂起协程,直到下一次恢复执行。
    • initial_suspendfinal_suspend 方法用于控制协程的初始和最终挂起行为。
    • get_return_object 方法返回一个 Generator 对象,该对象持有协程句柄。
    • return_voidunhandled_exception 方法处理协程的返回和异常情况。
  • 定义生成器协程 sequence
    • sequence 是一个生成器协程,从 start 开始,每次生成的值增加 step
    • 使用 co_yield 关键字将值逐步生成并返回给调用方。
  • main 函数中使用生成器
    • 创建一个 sequence 生成器,起始值为 0,步长为 5。
    • 使用 for 循环调用生成器的 next 方法生成值,并使用 getValue 方法获取当前值。
    • 输出生成的值,结果为:0 5 10 15 20 25 30 35 40 45

通过这种方式,标准生成器协程使得创建惰性序列变得更加容易和高效,开发者可以利用生成器协程按需生成序列元素,而不必一次性生成所有元素。这对于处理大数据集、流数据或需要逐步产生值的应用场景特别有用。

3.5. std::expected 作为错误处理的新方法

std::expected 提供了一种新的错误处理方式,作为异常的更具表达性和类型安全的替代方案。与传统的异常处理机制不同,std::expected 使得函数的返回值可以同时包含成功的结果或错误信息,从而提高了代码的可读性和健壮性。

示例代码:

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

std::expected<int, std::string> safe_divide(int a, int b) {
    if (b == 0) {
        return std::unexpected("Division by zero!");
    }
    return a / b;
}

int main() {
    auto result = safe_divide(10, 2);
    if (result) {
        std::cout << "Result: " << result.value() << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }

    result = safe_divide(10, 0);
    if (result) {
        std::cout << "Result: " << result.value() << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }

    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::expected 来处理函数可能的错误情况。以下是详细解析:

  • 定义 safe_divide 函数
    • safe_divide 函数接受两个整数参数 ab,返回一个 std::expected<int, std::string> 类型的值。
    • 如果 b 为 0,表示除零错误,函数返回 std::unexpected("Division by zero!"),其中包含错误信息。
    • 否则,函数返回 a / b 的结果。
  • main 函数中调用 safe_divide
    • 调用 safe_divide(10, 2),将结果存储在 result 变量中。由于 b 不为 0,返回成功结果 5
    • 使用 if (result) 检查返回值是否包含成功结果。如果是,输出结果值 result.value();否则,输出错误信息 result.error()
    • 再次调用 safe_divide(10, 0),此时由于 b 为 0,返回错误信息 "Division by zero!"
    • 同样使用 if (result) 检查返回值,输出错误信息 result.error()

通过这种方式,std::expected 提供了一种更直观和类型安全的错误处理方法,使得函数调用者能够明确地检查并处理可能的错误情况,而无需依赖异常机制。使用 std::expected 可以提高代码的可读性和维护性,特别是在需要处理多种错误类型或在性能敏感的应用中非常有用。

3.6. 运行时堆栈跟踪

C++23 引入了运行时堆栈跟踪功能,允许开发者在运行时获取堆栈跟踪信息,从而方便调试和错误诊断。通过 std::stacktrace,开发者可以轻松地打印当前的堆栈跟踪,了解程序执行路径,快速定位问题。

示例代码:

#include <iostream>
#include <stacktrace>

void foo() {
    std::cout << std::stacktrace::current() << std::endl;
}

int main() {
    foo();
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::stacktrace 进行运行时堆栈跟踪。以下是详细解析:

  • 引入头文件
    • #include <stacktrace> 引入了堆栈跟踪所需的头文件。
  • 定义函数 foo
    • foo 函数中,使用 std::stacktrace::current() 获取当前的堆栈跟踪信息。
    • 将堆栈跟踪信息通过 std::cout 打印出来,以便查看程序的调用路径。
  • main 函数中调用 foo
    • main 函数调用 foo,从而触发 foo 中的堆栈跟踪打印操作。
    • 运行程序时,堆栈跟踪信息会被打印到标准输出,显示出程序的调用路径。

堆栈跟踪示例输出

foo() at example.cpp:5
main() at example.cpp:10

(实际输出取决于编译器和平台)

通过这种方式,C++23 的运行时堆栈跟踪功能提供了一种方便的方式来获取和查看程序的堆栈信息。这对于调试复杂应用程序、定位错误和分析程序行为非常有用。开发者可以在程序的关键位置添加堆栈跟踪打印,以便在出现问题时快速诊断和解决。

4. 多线程和并发改进

4.1. std::jthread

std::jthread 是一个更易于使用的线程类,自动管理线程的生命周期。当 std::jthread 对象被销毁时,如果线程仍在运行,会自动调用 join() 方法,从而确保线程被正确地管理和终止,避免了资源泄露和未定义行为。

示例代码:

#include <iostream>
#include <thread>
#include <chrono>

void work() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Work done" << std::endl;
}

int main() {
    std::jthread t(work);
    // t.join() is automatically called at the end of scope
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::jthread 创建并管理线程。以下是详细解析:

  • 定义工作函数 work
    • work 函数模拟一个简单的工作负载,通过调用 std::this_thread::sleep_for(std::chrono::seconds(1)) 使线程休眠1秒钟,然后输出 "Work done"
  • 创建 std::jthread 对象
    • main 函数中,创建一个 std::jthread 对象 t,并将 work 函数传递给它作为线程的任务。
    • 线程 t 开始执行 work 函数,进入休眠状态。
  • 自动管理线程生命周期
    • main 函数结束时,std::jthread 对象 t 超出作用域,其析构函数被调用。
    • std::jthread 的析构函数会自动调用 join() 方法,确保线程 t 在程序退出之前完成其工作。
    • 这避免了手动调用 join() 的麻烦,并确保没有悬挂线程。

通过这种方式,std::jthread 提供了一种更简单和安全的线程管理方式,自动处理线程的生命周期,减少了错误和资源泄露的风险。这对于多线程编程特别有用,使代码更加简洁和健壮。

4.2. std::stop_tokenstd::stop_source

std::stop_tokenstd::stop_source 提供了一种标准化的机制来请求停止线程操作。std::jthread 可以与 std::stop_token 一起使用,使得线程可以响应停止请求,从而安全地终止线程。

示例代码:

#include <iostream>
#include <thread>
#include <chrono>
#include <stop_token>

void work(std::stop_token st) {
    while (!st.stop_requested()) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::cout << "Working..." << std::endl;
    }
    std::cout << "Stopped." << std::endl;
}

int main() {
    std::jthread t(work);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    t.request_stop();
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::stop_tokenstd::stop_source 来实现线程的可控停止。以下是详细解析:

  • 定义工作函数 work
    • work 函数接受一个 std::stop_token 参数 st,用于检查停止请求。
    • while 循环中,使用 st.stop_requested() 检查是否有停止请求。
    • 如果没有停止请求,线程每 100 毫秒输出一次 "Working..." 并继续工作。
    • 一旦接收到停止请求,跳出循环并输出 "Stopped."
  • 创建 std::jthread 对象
    • main 函数中,创建一个 std::jthread 对象 t,并将 work 函数传递给它作为线程的任务。
    • 线程 t 开始执行 work 函数。
  • 发送停止请求
    • main 函数中,主线程休眠 1 秒钟,以模拟某种条件下发出停止请求的延迟。
    • 使用 t.request_stop()work 线程发送停止请求。
    • work 线程在检测到停止请求后,跳出 while 循环,执行停止逻辑并输出 "Stopped."

通过这种方式,std::stop_tokenstd::stop_source 提供了一种安全和标准化的机制来请求和响应线程停止,使得线程可以有序和安全地终止。这在需要可控停止线程的多线程应用中非常有用,避免了强制终止线程带来的资源泄漏和不一致状态问题。

4.3. std::latchstd::barrier

新增的同步原语 std::latchstd::barrier 允许更高效地管理并发任务。std::latch 用于等待一组线程完成工作,而 std::barrier 用于协调一组线程的并行阶段性进展。

示例代码:

#include <iostream>
#include <thread>
#include <latch>
#include <vector>

void worker(std::latch& latch) {
    std::cout << "Worker is working...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    latch.count_down();
    std::cout << "Worker is done.\n";
}

int main() {
    std::latch latch(3);
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(worker, std::ref(latch));
    }
    latch.wait();
    std::cout << "All workers are done.\n";
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::latch 来同步多个线程的执行。以下是详细解析:

  • 定义工作函数 worker
    • worker 函数接受一个 std::latch& 参数,用于同步线程。
    • 每个工作线程输出 "Worker is working...",然后休眠 1 秒钟以模拟工作负载。
    • 调用 latch.count_down() 减少 latch 的计数,表示该线程已完成工作。
    • 最后输出 "Worker is done."
  • 创建 std::latch 对象
    • main 函数中,创建一个 std::latch 对象,初始计数为 3,表示需要等待 3 个线程完成工作。
  • 启动工作线程
    • 使用 std::vector<std::thread> 创建 3 个线程,每个线程执行 worker 函数,并传递 latch 引用。
    • 每个线程启动后,开始执行 worker 函数。
  • 等待所有线程完成
    • 调用 latch.wait(),主线程阻塞,直到 latch 的计数减为 0,表示所有工作线程已完成工作。
    • 输出 "All workers are done."
  • 等待线程结束
    • 使用 for 循环调用每个线程的 join() 方法,确保所有线程都已终止。

通过这种方式,std::latch 提供了一种简单高效的方式来同步多个线程的工作进度。std::latch 非常适合于等待一组线程完成初始化或特定任务,然后再继续后续操作的场景。

使用 std::barrier
std::barrier 用于在并行阶段之间同步一组线程,每个阶段结束时等待所有线程完成,然后同时开始下一阶段。

示例代码:

#include <iostream>
#include <thread>
#include <barrier>
#include <vector>

void phase_work(std::barrier<>& sync_point) {
    std::cout << "Phase 1 working...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    sync_point.arrive_and_wait();

    std::cout << "Phase 2 working...\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    sync_point.arrive_and_wait();
}

int main() {
    std::barrier sync_point(3);
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(phase_work, std::ref(sync_point));
    }
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::barrier 来同步线程的并行阶段。以下是详细解析:

  • 定义工作函数 phase_work
    • phase_work 函数接受一个 std::barrier& 参数,用于同步线程的并行阶段。
    • 第一个阶段,输出 "Phase 1 working..." 并休眠 1 秒钟,然后调用 sync_point.arrive_and_wait() 同步,等待所有线程完成第一阶段。
    • 第二个阶段,输出 "Phase 2 working..." 并休眠 1 秒钟,然后再次调用 sync_point.arrive_and_wait() 同步,等待所有线程完成第二阶段。
  • 创建 std::barrier 对象
    • main 函数中,创建一个 std::barrier 对象,初始计数为 3,表示需要等待 3 个线程同步。
  • 启动工作线程
    • 使用 std::vector<std::thread> 创建 3 个线程,每个线程执行 phase_work 函数,并传递 sync_point 引用。
    • 每个线程启动后,开始执行 phase_work 函数。
  • 等待线程结束
    • 使用 for 循环调用每个线程的 join() 方法,确保所有线程都已终止。

通过这种方式,std::barrier 提供了一种高效的机制来管理并行计算中的阶段性同步,使得线程可以在每个阶段结束时等待其他线程,然后同时开始下一阶段。

4.4. 任务块和协同任务

通过改进的任务调度机制,协同任务可以更高效地共享资源和时间片,提升并发执行的性能。在 C++23 中,任务块和协同任务使得开发者能够更灵活地管理并发任务,确保资源的高效利用和任务的协调执行。以下是一个示例代码,展示了如何使用任务块和协同任务来实现并发执行。

示例代码:

#include <iostream>
#include <thread>
#include <vector>
#include <latch>
#include <barrier>

// 定义一个简单的工作函数
void work(std::latch& start_latch, std::barrier<>& end_barrier, int id) {
    // 等待所有线程准备就绪
    start_latch.wait();
    std::cout << "Worker " << id << " is working...\n";
    
    // 模拟工作负载
    std::this_thread::sleep_for(std::chrono::seconds(1));

    // 通知任务完成
    end_barrier.arrive_and_wait();
    std::cout << "Worker " << id << " has finished.\n";
}

int main() {
    const int num_threads = 5;
    std::latch start_latch(num_threads);
    std::barrier end_barrier(num_threads + 1); // 主线程也参与

    // 创建并启动线程
    std::vector<std::thread> threads;
    for (int i = 0; i < num_threads; ++i) {
        threads.emplace_back(work, std::ref(start_latch), std::ref(end_barrier), i + 1);
        start_latch.count_down(); // 准备就绪,计数减一
    }

    // 等待所有线程完成工作
    end_barrier.arrive_and_wait();
    std::cout << "All workers have finished.\n";

    // 等待所有线程结束
    for (auto& t : threads) {
        t.join();
    }

    return 0;
}

示例解析:

在这个示例中,我们创建了五个线程,每个线程都执行相同的 work 函数。std::latch 用于确保所有线程在开始工作之前都已经就绪,而 std::barrier 则用于协调线程在完成工作后的同步。

  • 初始化 std::latchstd::barrier
    • std::latch 的初始计数为线程数,确保所有线程在开始工作之前都已经就绪。
    • std::barrier 的初始计数为线程数加一,因为主线程也参与同步。
  • 创建并启动线程
    • 每个线程在启动时,都会调用 start_latch.count_down() 来减少计数,表示自己已经准备就绪。
    • 线程开始工作前,会等待 start_latch.wait(),确保所有线程都已启动。
  • 执行任务并同步
    • 每个线程在执行完工作负载后,调用 end_barrier.arrive_and_wait() 通知任务完成,并等待其他线程完成。
  • 主线程等待并同步
    • 主线程也调用 end_barrier.arrive_and_wait(),等待所有工作线程完成任务。
  • 等待线程结束
    • 使用 join() 确保所有线程都已结束执行。

通过这种方式,任务块和协同任务机制可以高效地管理并发执行,确保任务的协调和资源的高效利用。

4.5. 执行策略的改进

C++23 增强了标准库中的执行策略,包括并行和异步执行的支持。这些改进使得开发者能够更高效地利用多核处理器,提升程序的性能。通过使用新的执行策略,开发者可以轻松实现并行算法,大幅减少计算时间。

示例代码:

#include <algorithm>
#include <execution>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec(1000000, 1);
    std::transform(std::execution::par, vec.begin(), vec.end(), vec.begin(),
                   [](int x) { return x * 2; });

    std::cout << "Transformation done.\n";
    return 0;
}

示例解析:
在这个示例中,使用了 C++23 增强的标准库执行策略之一:std::execution::par。这使得 std::transform 算法能够并行地执行,从而显著提高了性能。以下是对示例的详细解析:

  • 初始化数据
    • 创建一个包含 1,000,000 个元素的向量 vec,每个元素的值为 1。
  • 并行执行转换
    • 使用 std::transform 算法,将 vec 中的每个元素乘以 2。这里的关键是使用 std::execution::par 执行策略,使得算法能够并行执行。
    • std::execution::par 指示标准库在多个线程上并行执行该算法,从而利用多核处理器的计算能力,减少总体执行时间。
  • 输出结果
    • 转换完成后,输出 "Transformation done.",表示并行操作已完成。

通过这种方式,C++23 的执行策略改进使得并行和异步执行变得更加简单和高效。开发者可以利用这些特性,在不改变算法逻辑的情况下,显著提升计算密集型任务的性能。这些改进对需要处理大量数据或进行复杂计算的应用程序尤其有用,例如图像处理、数据分析和科学计算等领域。

5. 实用工具和功能

5.1. std::printstd::println

C++23 引入了新的输出函数 std::printstd::println,简化了输出操作。这些函数相比于传统的 printfstd::cout 更加易用,提供了更简洁的语法和格式化能力。

示例代码:

#include <print>

int main() {
    std::print("Hello, World!\n");
    std::println("Formatted number: {}", 42);
    return 0;
}

示例解析:
在这个示例中,展示了如何使用 std::printstd::println 来进行输出操作。以下是详细解析:

  • 引入头文件
    • #include <print> 引入了新的输出函数所需的头文件。
  • 使用 std::print 输出字符串
    • std::print("Hello, World!\n"); 直接输出字符串 "Hello, World!" 并换行。
    • std::print 提供了类似于 printf 的功能,但不需要指定格式化类型。
  • 使用 std::println 格式化输出
    • std::println("Formatted number: {}", 42); 使用了大括号 {} 作为占位符,输出格式化的字符串。
    • 这里 42 被格式化为字符串插入到占位符 {} 位置,输出结果为 "Formatted number: 42"
    • std::println 自动在末尾添加换行符,比 std::print 更加方便。

通过这种方式,std::printstd::println 提供了一种更简洁和易用的输出操作方法。与传统的 printfstd::cout 相比,这些函数减少了代码的复杂性,使得格式化输出变得更加直观和高效,特别适合日常开发中的简单输出需求。这些新函数增强了 C++ 标准库的易用性,帮助开发者编写更清晰和可维护的代码。

5.2. Ranges库的更新

C++23 对 Ranges 库进行了许多更新和增强,使其更加强大和灵活,适用于各种集合操作。新增了许多便捷的适配器和算法,以简化代码编写并提高代码可读性和性能。以下是一个示例代码,展示了如何使用这些新功能。

示例代码:

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

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

    // 使用新的 ranges 适配器
    auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
    auto squared_numbers = even_numbers | std::views::transform([](int n) { return n * n; });

    for (int n : squared_numbers) {
        std::cout << n << ' ';
    }
    std::cout << std::endl;

    // 新的 range 算法
    bool contains_five = std::ranges::contains(numbers, 5);
    std::cout << "Contains 5: " << std::boolalpha << contains_five << std::endl;

    return 0;
}

示例解析:
在这个示例中,展示了如何使用 C++23 Ranges 库的更新功能来进行集合操作。以下是详细解析:

  • 定义数据集合
    • 创建一个包含 10 个整数的 std::vector<int> 类型的变量 numbers,用于存储数据集合。
  • 使用 ranges 适配器
    • 使用 std::views::filter 适配器过滤出所有偶数元素。filter 适配器接受一个谓词(lambda 函数 [](int n) { return n % 2 == 0; }),只保留符合条件的元素。
    • 使用 std::views::transform 适配器将偶数元素平方。transform 适配器接受一个转换函数(lambda 函数 [](int n) { return n * n; }),对每个元素应用该函数。
  • 遍历并输出结果
    • 使用范围 for 循环遍历 squared_numbers,并输出每个元素的值。结果为 4 16 36 64 100,即 2^2 4^2 6^2 8^2 10^2
  • 使用 ranges 算法
    • 使用 std::ranges::contains 算法检查 numbers 集合中是否包含值 5contains 算法返回一个布尔值。
    • 输出检查结果 Contains 5: true,表示集合中包含值 5

通过这种方式,C++23 Ranges 库的更新使得集合操作更加简洁和高效。开发者可以利用这些新的适配器和算法编写更具可读性和维护性的代码,特别适用于需要对数据集合进行复杂操作的场景。这些增强功能显著提高了代码的表达能力和执行性能。

5.3. 单元操作与改进

C++23 增强了对单元操作的支持,改进了标准库中某些类型的功能,使其更符合实际应用需求。例如,改进了 std::optionalstd::variant 等类型的操作,增加了更丰富的成员函数和操作符重载,使得这些类型的使用更加便捷和高效。

示例代码:

#include <iostream>
#include <optional>
#include <variant>

int main() {
    std::optional<int> opt = 42;
    if (opt) {
        std::cout << "Optional value: " << *opt << std::endl;
    }

    std::variant<int, std::string> var = "Hello, World!";
    std::cout << "Variant value: " << std::get<std::string>(var) << std::endl;

    var = 2023;
    std::cout << "Variant value: " << std::get<int>(var) << std::endl;

    return 0;
}

示例解析:
在这个示例中,展示了如何使用 C++23 对 std::optionalstd::variant 的增强功能。以下是详细解析:

  • std::optional 操作
    • std::optional<int> opt = 42; 定义了一个 std::optional 类型的变量 opt,并初始化为 42
    • 使用 if (opt) 检查 opt 是否包含值。如果 opt 包含值,则输出该值。
    • C++23 增强了 std::optional 的操作,使得检查和访问可选值更加简洁和直观。
  • std::variant 操作
    • std::variant<int, std::string> var = "Hello, World!"; 定义了一个 std::variant 类型的变量 var,并初始化为字符串 "Hello, World!"
    • 使用 std::get<std::string>(var) 获取 var 中的字符串值,并输出。
    • var 赋值为整数 2023,再次使用 std::get<int>(var) 获取 var 中的整数值,并输出。
    • C++23 增强了 std::variant 的操作,使得类型安全的访问和修改更加方便。

通过这种方式,C++23 的单元操作与改进使得 std::optionalstd::variant 的使用更加高效和易于理解。开发者可以利用这些增强功能编写更加简洁和可维护的代码,特别是在需要处理可选值和多态值的场景中,提高了代码的灵活性和可读性。这些改进对实际应用中的数据处理和错误处理提供了更好的支持。

6. 总结

C++23 通过一系列新的语言特性和标准库增强,提高了 C++ 的可用性和开发效率。虽然其变化不如 C++20 那样显著,但它在完善和优化现有功能方面做出了重要贡献,为开发者提供了更加强大和灵活的工具。这些改进使得开发者能够编写出更简洁、高效和安全的代码,有助于提升软件开发的整体质量和效率。

7. 参考资料


本主页会定期更新,为了能够及时获得更新,敬请关注我:点击左下角的关注。也可以关注公众号:请在微信上搜索公众号“AI与编程之窗”并关注,或者扫描以下公众号二维码关注,以便在内容更新时直接向您推送。 

;