C++ 23 新特性概览之 标准库

简介

本文简要介绍了一下C++23中标准库部分新增的特性. 为了代码简短, 本文中默认使用了using namespace std;, 实际工作中不推荐这样做.

另外为了更美观的打印, 使用了fmt库, fmt::命名空间的函数均为其库函数.

关于环境

  • 本文中一部分代码是在Microsoft Visual Studio 2022 Preview版本上运行的. 其中开启的编译选项为
    • /std:c++latest: 支持当前最新的标准
    • /experimental:module: 支持C++ Module
  • 另一部分代码是在Compiler Explorer上运行的.
    • Compiler Explorer网站支持运行GCC, Clang的编译和运行, 不支持MSVC的运行, 仅仅支持编译
  • 对于一些完全没有编译器支持的功能, 则只是简单的介绍概念, 无可运行的代码.

字符串格式化改进

C++23支持在std::print()以及std::println()中格式化字符串.

以下是一个简单的例子:

std::string name{ "C++23" };
// Old-style cout/format() pattern
std::cout << std::format("Hello {}!\n", name);
// C++23 print()
std::print("Hello {}!\n", name);
// C++23 println()
std::println("Hello {}!", name);

目前没有编译器支持. 如果需要使用可以使用fmt库.

vector<pair<int, int>> v { {1, 2}, { 3, 4 } };
println("{}", v);   // [(1, 2), (3, 4)]

set<pair<int, int>> s { {1, 2}, { 3, 4 } };
println("{}", s);   // {(1, 2), (3, 4)}

map<int, int> m { {1, 2}, { 3, 4 } };
println("{}", m);   // {1: 2, 3: 4}

vector<string> strings{ "Hello", "World!\t2023" };
println("{}", strings);        // ["Hello", "World!\t2023"]
println("{::}", strings);      // [Hello, World!    2023]

vector<vector<int>> vv{ {11, 22}, { 33, 44, 55 } };
println("{}", vv);             // [[11, 22], [33, 44, 55]]
println("{:n}", vv);           // [11, 22], [33, 44, 55]
println("{:n:n}", vv);         // 11, 22, 33, 44, 55
println("{:n:n:*^4}", vv);     // *11*, *22*, *33*, *44*, *55*

标准库模块

C++23引入了标准库模块(Module),这是一个新的模块化标准库的方式,它将标准库分解为多个模块,每个模块都有自己的接口和实现, 这样可以减少编译时间,提高构建速度。

import std

import std导入的模块(Module)包括:

  • C++ 标准库的所有模块, 比如vector, string, cout
  • C++ 中关于C语言的包装, 比如fopen()
import std;

using namespace std;

int main() {
  cout << "Hello from cout!" << endl;
  println("Hello from println!");
  printf("%s\n", "Hello from cstdlib!");
  return 0;
}

将会输出:

Hello from cout!
Hello from std::println!
Hello from cstdlib!

import std.compat

目前没有编译器支持.

除了std 之外会加上全局命名空间的符号.

示例代码:

import std.compat;

int main() {
    std::cout << "Hello from std::cout" << std::endl;
    std::println("Hello from std::println!");
    std::printf("Hello from std::printf\n");

    ::printf("Hello from ::printf\n");
    return 0;
}

basic_string(_view)::contains()

检查字符串中是否含有待查找串.

Example:

#include <fmt/core.h>

#include <string>

int main() {
  std::string haystack{"Hello World!"};
  fmt::println("{}", haystack.contains("World"));  // true
  fmt::println("{}", haystack.contains('!'));      // true
  using namespace std::string_view_literals;
  fmt::println("{}", haystack.contains("Hello"sv));  // true
}

在Compiler Explorer运行代码

禁止从 nullptr 构造 string(_view)

在C++20及之前的版本中, 以下代码是合法的:

// C++20 and before (undefined behavior)
std::string s { nullptr };

但是在运行时会有未定义行为.

C++23中禁止了从nullptr构建字符串.

// C++23 (compile-time error)
std::string s { nullptr };

但是还是可以从空指针构建字符串:

const char* p = nullptr;
std::string s{ p };
assert(s.empty()); // runtime assertion

basic_string::resize_and_overwrite(count, op)

  • count <= size(), 删除多余的元素
  • count > size(), 添加 count - size() 个默认初始化的元素
    • 触发 r = op(data(), count)
    • 触发 erase(begin() + r, end())

考虑一个将字符串pattern重复count次的函数:

std::string GeneratePattern(const std::string& pattern, size_t count) {
   std::string result;
   result.reserve(pattern.size() * count);
   for (size_t i = 0; i < count; i++) {
      result.append(pattern);
   }
   return result;
}

这样的实现不是最优的, 因为:

  • 写入countnull
  • 更新size并检查潜在的resize count
std::string GeneratePattern(const std::string& pattern, size_t count) {
   std::string result;
   const auto step = pattern.size();
   // GOOD: No initialization
   result.resize_and_overwrite(step * count, [&](char* buf, size_t n) {
      for (size_t i = 0; i < count; i++) {
         // GOOD: No bookkeeping
         memcpy(buf + i * step, pattern.data(), step);
      }
      return step * count;
   });
   return result;
}

std::optional 的链式调用

C++23中新增了一些支持链式调用的函数:

  • transform(F): 如果*this有值, 则返回F的结果, 否则返回空的optional
  • and_then(F): 如果*this有值, 则返回F的结果, 否则返回空的optional
  • or_else(F): 如果*this有值, 则返回*this, 否则返回F的结果
#include <iostream>
#include <optional>
#include <string>
#include <vector>

using namespace std;

optional<int> Parse(const string& s) {
  try {
    return stoi(s);
  } catch (...) {
    return {};
  }
}

int main() {
  vector<string> inputs{"12", "nan"};
  for (auto& s : inputs) {
    auto result =
        Parse(s)
            .and_then([](int value) -> optional<int> { return value * 2; })
            .transform([](int value) { return to_string(value); })
            .or_else([] { return optional<string>{"No Integer Found"}; });
    cout << *result << endl;
  }
}

在Compiler Explorer运行代码

Stacktrace

定义在 <stacktrace> 头文件中.
允许获取和处理堆栈跟踪.

样例代码:

auto trace { std::stacktrace::current() };
std::cout << std::to_string(trace) << std::endl;

g++ 编译时需要加 -lstdc++_libbacktrace 选项. 如果gcc版本大于等于14, 选项改为-lstdc++exp
样例输出:

0# bar() at /workspaces/cmake-project-2024/src/cpp23/stacktrace.cpp:6
1# foo() at /workspaces/cmake-project-2024/src/cpp23/stacktrace.cpp:14
2# main at /workspaces/cmake-project-2024/src/cpp23/stacktrace.cpp:16
3# __libc_start_call_main at :0
4# __libc_start_main_impl at :0
5# _start at :0
6#

在Compiler Explorer运行代码

在异常中增加堆栈信息

很多语言都支持在出现异常的时候打印堆栈, 更容易定位出错位置, 方便调试. C++23中也支持这个功能.

样例代码:

#include <exception>
#include <iostream>
#include <stacktrace>

class Exception : public std::exception {
 public:
  explicit Exception(std::string message,
                     std::stacktrace st = std::stacktrace::current())
      : message_{std::move(message)}, stack_{std::move(st)} {}
  [[nodiscard]] const char* what() const noexcept override {
    return message_.c_str();
  }
  [[nodiscard]] const std::stacktrace& trace() const noexcept { return stack_; }

 private:
  std::string message_;
  std::stacktrace stack_;
};

void bar() { throw Exception{"exception at bar()."}; }
void foo() { bar(); }

int main() {
  try {
    foo();
  } catch (const Exception& e) {
    std::cout << "Exception caught: " << e.what() << std::endl;
    std::cout << "Stacktrace: \n" << std::to_string(e.trace()) << std::endl;
  }
}

在Compiler Explorer运行代码

进一步阅读:

Ranges 库的变化

ranges::starts_with() / ranges::ends_with()

检查一个range的开头(或结尾)是否与另一个range匹配.

样例代码:

vector v1{11, 22, 33, 44};
vector v2{11, 22};
fmt::println("{}", ranges::starts_with(v1, v2));  // true
fmt::println("{}", ranges::ends_with(v1, v2));    // false

ranges::shift_left() / ranges::shift_right()

将一个range的元素向左或向右移动.
样例代码:

vector<string> v{"a", "b", "c", "d", "e"};
ranges::shift_left(v, 2);
fmt::println("{}", v);  // "c", "d", "e", "", ""
ranges::shift_right(v, 1);
fmt::println("{}", v);  // "", "c", "d", "e", ""

ranges::to()

将一个range内的元素转储到一个容器.

样例代码:

auto ints = std::views::iota(1, 5)
            | std::views::transform([](const auto& v) { return v * 2; });
auto vec{ std::ranges::to<std::vector>(ints) };
std::print("{}", vec); // [2, 4, 6, 8]

// 从vector 转到set
auto set1{ std::ranges::to<std::set>(vec) };

// 用管道符从set<int> 转到 set<double>
auto set2{ vec | std::ranges::to<std::set<double>>() };

// 使用`from_range`构造函数从vector<int> 转到set<double>
std::set<double> set3{ std::from_range, vec };

ranges::split()

使用views::split()将一个字符串分割成多个子串.

string text{"s1 s2 s3 s4 s5"};

auto words{text | views::split(' ') | views::transform([](const auto& v) {
            return string{from_range, v};
            }) |
            ranges::to<vector>()};

fmt::println("words: {:n:?}", words);  // words: "s1", "s2", "s3", "s4", "s5"

ranges::find_last()系列

查找一个range中的最后一个元素.

  • ranges::find_last(): 如果匹配一个指定元素
  • ranges::find_last_if(): 如果给定的谓词返回true
  • ranges::find_last_if_not(): 如果给定的谓词返回false

返回一个子range的迭代器, 一直到范围结束. 如没有找到则返回{last, last}

样例代码:

vector v{1, 2, 3, 4, 5};
fmt::println("{}", ranges::find_last(v, 3));  // [3, 4, 5]
fmt::println("{}", ranges::find_last_if(
             v, [](int i) { return i % 2 == 0; }));  // [4, 5]
fmt::println("{}", ranges::find_last_if_not(
             v, [](int i) { return i % 2 == 0; }));  // [5]

ranges::contains() / ranges::contains_subrange()

  • ranges::contains(): 检查是否含有一个元素
  • ranges::contains_subrange(): 检查是否含有一个range

样例代码:

std::vector v1{ 11, 22, 33, 44 };
std::vector v2{ 33, 44 };
std::println("{}", std::ranges::contains(v1, 22)); // true
std::println("{}", std::ranges::contains_subrange(v1, v2)); // true

一些folding算法

新增了如下算法:

  • ranges::fold_left()
  • ranges::fold_left_first()
  • ranges::fold_right()
  • ranges::fold_right_last()
  • ranges::fold_left_with_iter()
  • ranges::fold_left_first_with_iter()

样例代码:

vector v{1, 2, 3, 4, 5};
fmt::println("{}", ranges::fold_left(v, 0, std::plus{})); // 15
fmt::println("{}", std::ranges::fold_left_first(v, std::multiplies<>()).value()); // 120
fmt::println("{}", ranges::fold_right(v, 1, std::multiplies{})); // 120
fmt::println("{}", ranges::fold_right_last(v, std::plus{}).value()); // 15
fmt::println("{}", ranges::fold_left_with_iter(v, 0, std::plus{}).value); // 15
fmt::println("{}", ranges::fold_left_first_with_iter(v, std::multiplies{}).value.value()); // 120

Views 库的变化

views::zip

views::zip是一个新的视图, 它可以将多个视图合并成一个视图.

样例代码:

vector v1{1, 2, 3};
vector v2{'a', 'b', 'c'};
auto r1{views::zip(v1, v2)};
fmt::println("{}", r1); // [(1, 'a'), (2, 'b'), (3, 'c')]

view::zip_transform

views::zip_transform是一个新的视图, 它可以将多个视图的元素应用一个操作符, 生成一个新的视图.

样例代码:

vector v1{1, 2, 3};
vector v2{4, 5, 6};
auto r2{views::zip_transform(multiplies(), v1, v2)};
fmt::println("{}", r2); // [4, 10, 18]

views::adjacent

views::adjacent是一个新的视图, 它可以将一个视图的元素组合成一个元组.
views::adjacent: A view with each element a tuple of references to N adjacent elements from the original view

vector v{1, 2, 3, 4};
auto r3{v | views::adjacent<2>};
fmt::println("{}", r3); // [(1, 2), (2, 3), (3, 4)]

views::adjacent_transform

将操作施于相邻的N个元素, 并将结果存储在新的视图中.

样例代码:

vector v{1, 2, 3, 4};
auto r4{v | views::adjacent_transform<2>(multiplies())};
fmt::println("{}", r4); // [2, 6, 12]

views::pairwise & views::pairwise_transform

这两个是工具函数:

  • views::pairwise = views::adjacent<2>
  • views::pairwise_transform = views::adjacent_transform<2>

样例代码:

vector v2{1, 2, 3, 4};
auto r5{v2 | views::pairwise};
fmt::println("{}", r5); // [(1, 2), (2, 3), (3, 4)]

vector v3{3, 4, 5};
auto r6{v3 | views::pairwise_transform(plus())};
fmt::println("{}", r6); // [7, 9]

views::slide

views::slide是一个新的视图, 它可以将一个视图的元素组合成一个元组.

views::adjacent相似, 但是窗口大小是运行时参数.

样例代码:

vector v{1, 2, 3, 4, 5};
auto r7{v | views::slide(2)};
fmt::println("{}", r7);  // [[1, 2], [2, 3], [3, 4], [4, 5]]

views::chunk

创建一个新的视图, 每N个元素组成一个元组.

样例代码:

vector v { 1, 2, 3, 4, 5 };
auto r8 { v | views::chunk(2) };
fmt::println("{}", r8);  // [[1, 2], [3, 4], [5]]

views::chunk_by

创建一个新的视图, 但是每个元组的元素是通过一个谓词来决定的.

样例代码:

vector v{1, 2, 2, 3, 0, 4, 5, 2};
auto r9{v | views::chunk_by(ranges::less_equal{})};
fmt::println("{}", r9);  // {(1,2,2,3),(0,4,5),(2)}

views::join_with

使用给定的分隔符连接一个视图的元素.
views::join_with(): Joins elements of a range using a given separator
样例代码:

vector<string> vs{"hi", "you", "!"};
auto str{vs | views::join_with('\n')};
fmt::println("{}", str); // ['h', 'i', '\n', 'y', 'o', 'u', '\n', '!']

views::stride

返回一个视图, 其中的元素是原视图的等间隔的子集.

样例代码:

vector v { 1, 2, 3, 4, 5 };
auto r10 { v | views::stride(2) };
fmt::println("{}", r10);  // {1, 3, 5}

views::repeat

重复一个元素无数次, 或者指定次数.

auto r11 { views::repeat(2, 3) }; // {2, 2, 2}
auto r12 { views::repeat(2) };    // {2, 2, 2, ... }

views::cartesian_product

返回表示n个给定范围的笛卡尔积的元组的视图.

样例代码:

vector v = { 1, 2 };
auto r13 { views::cartesian_product(v, v) };
fmt::println("{}", r13); // [(1, 1), (1, 2), (2, 1), (2, 2)]

views::as_rvalue

一个表示底层range的视图, 但是其元素是右值.

vector words{"Hello"s, "World"s, "2023"s};
vector<string> mv;
ranges::copy(words | views::as_rvalue, back_inserter(mv));
fmt::println("{}", mv);  // ["Hello", "World", "2023"]

在Compiler Explorer运行代码

expected

定义在头文件 <expected>中.

expected<T, E> 包含两个值:

  • 一个类型为T的值, 期望的值
  • 一个类型为E的值, 错误值
    保证不会为空.
    unexpected() 用于创建一个意外的值.

样例代码:

expected<int, string> a { 21 };
expected<int, string> b { unexpected("Some error"s) };

成员函数:

  • has_value(): 如果含有值, 返回true, 否则返回false
  • value(): 返回引用到包含的值, 如果没有值, 抛出bad_expected_access
  • error(): 返回指向错误的引用

链式调用

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

using namespace std;

expected<int, string> divide(int a, int b) {
  if (b == 0) {
    return unexpected<string>("Division by zero");
  } else {
    return a / b;
  }
}

int main() {
  auto result =
      divide(12, 2)
          .and_then([](int result) {
            return divide(result, 2);  // Further divide the result by 2
          })
          .or_else([](const string& error) {
            std::cout << "Error: " << error << std::endl;
            return expected<int, std::string>(
                0);  // Return a default value in case of error
          });

  if (result) {
    std::cout << "Result: " << *result << std::endl;
  } else {
    std::cout << "Error: " << result.error() << std::endl;
  }

  return 0;
}

在Compiler Explorer运行代码

std::move_only_function<>

在C++23之前, 这个例子会失败:

int Work(std::function<int()> f) {
   return f();
}
std::cout << Work([p = std::make_unique<int>(42)] { return *p; });
“Attempting to reference a deleted function”
The copy ctor of the std::function tries to copy the lambda, which is not possible due to the captured unique_ptr

C++23:

#include <fmt/core.h>

#include <functional>
#include <iostream>
using namespace std;

int Process(std::move_only_function<int()> f) { return f(); }

int main() {
  cout << Process([] { return 1; }) << endl; // 1
  cout << Process([p = std::make_unique<int>(2)] { return *p; }) << endl; // 2
}

在Compiler Explorer运行代码

std::spanstream

定义在头文件 <spanstream>中. 允许在外部缓冲区上使用流操作.

样例代码:

char text[] = "11 22";
int a, b;
ispanstream in{span<char>{text}};
in >> a >> b;
fmt::println("a: {} b: {}", a, b);

char buffer[32]{};
ospanstream out{span<char>{buffer}};
out << 22 << 11;
fmt::println("{}", buffer);

输出

a: 11 b: 22
2211

在Compiler Explorer运行代码

查看源码

std::byteswap()

定义在头文件<bit>中. 是交换整数类型字节的标准方法.

样例代码:

std::uint32_t a{ 0x12345678u };
fmt::println("{:x}", a);
std::uint32_t b{ std::byteswap(a) };
fmt::println("{:x}", b);

输出

12345678
78563412

在Compiler Explorer运行代码

std::to_underlying()

定义在头文件<utility>中. 作用是将一个枚举转换为其基础类型, 与static_cast<std::underlying_type_t<E>>(enum_value)等效.

样例代码:

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

using namespace std;

enum Options : uint32_t { Opt1 = 1, Opt2 = 2, Opt3 = 3 };

int main() {
  Options r{Options::Opt1};

  auto a{static_cast<underlying_type_t<Options>>(r)};
  auto b{to_underlying(r)};
  assert(a == b);
  cout << a << endl;
  cout << b << endl;
}

在Compiler Explorer运行代码

std::flat_(multi)map & std::flat_(multi)set

目前为止没有编译器支持. 如果需要使用可以使用boost库.

std::flat_mapstd::flat_multimap

定义在 <flat_map> 头文件中.

提供类似于 std::map 的接口, 但是不支持重复的键, 并且键是有序的.
键的查询很快, 因为键是有序的.

内部存储是一个连续性的容器, 比如 std::vector 或者 std::deque. keyvalue分别存储在不同的容器上.

std::flat_map<int, std::string> myMap;
myMap[2023] = "CppCon"s;

std::flat_map<int, std::string,
              std::less<int>,
              std::deque<int>,
              std::deque<std::string>> myMap;

std::flat_setstd::flat_multiset

定义在 <flat_set> 头文件中.
内部的键存储在一个连续的有序的容器中.

std::flat_set<int> mySet;

std::mdspan

目前为止没有编译器支持.

定义在 <mdspan> 头文件中.

  • std::mdspan: 一个多维数组的视图, 是对std::span(C++20)的多维扩展.
    支持不同的布局策略.
  • std::submdspan: 一个对现有mdspan的子集的视图(切片)

样例代码:

int* data { /* ... */ };

// View data as contiguous memory representing 2 rows of 2 ints each
auto mySpan { std::mdspan(data, 2, 2) };

// Access data from mdspan
for (size_t i { 0 }; i < mySpan.extents().extent(0); ++i) {
   for (size_t j { 0 }; j < mySpan.extents().extent(1); ++j) {
      mySpan[i, j] = i * 1000 + j;
   }
}

std::generator

定义在 <generator> 头文件中. 定义了一个标准的协程生成器.

目前为止没有编译器支持.

样例代码:

std::generator<int> getSequenceGenerator(int startValue, int numberOfValues) {
   for (int i { startValue }; i < startValue + numberOfValues; ++i) {
      // Yield a value to the caller, and suspend the coroutine.
      co_yield i;
   }
}
int main() {
   auto gen { getSequenceGenerator(10, 5) };
   for (const auto& value : gen) {
      std::print("{} (Press enter for next value)", value);
      std::cin.ignore();
   }
}

关联容器异构删除

关联容器已经存在异构查找, C++23中新增了异构删除和提取, 也就是增加了一个erase(K&&)extract(K&&). 用来提升性能.

参考

后记

C++23中的新特性还有很多, 本文只是列举了一部分. 有些特性还没有得到编译器的支持, 有些特性还在讨论中. 本文只是一个概览, 详细的特性还需要查阅官方文档.

这篇文章也会进行更新补充, 欢迎关注.

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐