Overview of C++23 Features
新年伊始,要说什么选题最合适,那无疑是C++23了。
去年只写过Ranges和几个小特性的提案,对于其他特性,就全放在此篇一览究竟。
23是个小版本,主要在于「完善」二字,而非「新增」。因此,值得单独拿出来写篇文章的特性其实并不多,大多特性都是些琐碎小点,三言两语便可讲清。
本篇包含绝大多数C++23特性,难度三星就表示只会介绍基本用法,但有些特性的原理也会深入讲讲。
1
Deducing this(P0847)
Deducing this是C++23中最主要的特性之一。msvc在去年3月份就已支持该特性,可以在v19.32之后的版本使用。
为什么我们需要这个特性?
大家知道,成员函数都有一个隐式对象参数,对于非静态成员函数,这个隐式对象参数就是this指针;而对于静态成员函数,这个隐式对象参数被定义为可以匹配任何参数,这仅仅是为了保证重载决议可以正常运行。
Deducing this所做的事就是提供一种将非静态成员函数的「隐式对象参数」变为「显式对象参数」的方式。为何只针对非静态成员函数呢?因为静态成员函数并没有this指针,隐式对象参数并不能和this指针划等号,静态函数拥有隐式对象参数只是保证重载决议能够正常运行而已,这个参数没有其他用处。
于是,现在便有两种写法编写非静态成员函数。
1struct S_implicit {
2 void foo() {}
3};
4
5struct S_explicit {
6 void foo(this S_explicit&) {}
7};
通过Deducing this,可以将隐式对象参数显式地写出来,语法为this+type。
该提案最根本的动机是消除成员函数修饰所带来的冗余,举个例子:
1// Before
2struct S_implicit {
3 int data_;
4
5 int& foo() & { return data_; }
6 const int& foo() const& { return data_; }
7};
8
9// After
10struct S_explicit {
11 int data_;
12
13 template <class Self>
14 auto&& foo(this Self& self) {
15 return std::forward<Self>(self).data_;
16 }
17};
1int main() {
2 auto gcd = [](this auto self, int a, int b) -> int {
3 return b == 0 ? a : self(b, a % b);
4 };
5
6 std::cout << gcd(20, 30) << "\n";
7}
1//// Before
2// CRTP
3template <class Derived>
4struct Base {
5 void foo() {
6 auto& self = *static_cast<Derived*>(this);
7 self.bar();
8 }
9};
10
11struct Derived : Base<Derived> {
12 void bar() const {
13 std::cout << "CRTP Derived\n";
14 }
15};
16
17////////////////////////////////////////////
18//// After
19// Deducing this
20struct Base {
21 template <class Self>
22 void foo(this Self& self) {
23 self.bar();
24 }
25};
26
27struct Derived : Base {
28 void bar() const {
29 std::cout << "Deducing this Derived\n";
30 }
31};
1// Library
2namespace mylib {
3
4 struct S {
5 auto abstract_interface(this auto& self, int param) {
6 self.concrete_algo1(self.concrete_algo2(param));
7 }
8 };
9} // namespace mylib
10
11namespace userspace {
12 struct M : mylib::S {
13 auto concrete_algo1(int val) {}
14 auto concrete_algo2(int val) const {
15 return val * 6;
16 }
17 };
18} // namespace userspace
19
20int main() {
21 using userspace::M;
22 M m;
23 m.abstract_interface(4);
24}
1#include <iostream>
2#include <type_traits>
3#include <utility> // for std::forward_like
4
5auto get_message() {
6 return 42;
7}
8
9struct Scheduler {
10 auto submit(auto&& m) {
11 std::cout << std::boolalpha;
12 std::cout << std::is_lvalue_reference<decltype(m)>::value << "\n";
13 std::cout << std::is_rvalue_reference<decltype(m)>::value << "\n";
14 return m;
15 }
16};
17
18int main() {
19 Scheduler scheduler;
20 auto callback = [m=get_message(), &scheduler](this auto&& self) -> bool {
21 return scheduler.submit(std::forward_like<decltype(self)>(m));
22 };
23 callback(); // retry(callback)
24 std::move(callback)(); // try-or-fail(rvalue)
25}
26
27// Output:
28// true
29// false
30// false
31// true
1struct S {
2 int data_;
3 int foo(); // implicit this pointer
4 // int foo(this S); // Pass this by value
5};
6
7int main() {
8 S s{42};
9 return s.foo();
10}
11
12// implicit this pointer生成的汇编代码:
13// sub rsp, 40 ; 00000028H
14// lea rcx, QWORD PTR s$[rsp]
15// mov DWORD PTR s$[rsp], 42 ; 0000002aH
16// call int S::foo(void) ; S::foo
17// add rsp, 40 ; 00000028H
18// ret 0
19
20// Pass this by value生成的汇编代码:
21// mov ecx, 42 ; 0000002aH
22// jmp static int S::foo(this S) ; S::foo
2
Monadic std::optional(P0798R8)
map:对optional的值应用一个函数,返回optional中wrapped的结果。若是optional中没有值,返回一个空的optional; and_then:组合使用返回optional的函数; or_else:若是有值,返回optional;若是无值,则调用传入的函数,在此可以处理错误。
1// Before
2if (opt_string) {
3 std::optional<int> i = stoi(*opt_string);
4}
5
6// After
7std::optional<int> i = opt_string.and_then(stoi);
1// chain a series of functions until there's an error
2std::optional<string> opt_string("10");
3std::optional<int> i = opt_string
4 .and_then(std::stoi)
5 .transform([](auto i) { return i * 2; });
1// fails, transform not called, j == nullopt
2std::optional<std::string> opt_string_bad("abcd");
3std::optional<int> j = opt_string_bad
4 .and_then(std::stoi)
5 .transform([](auto i) { return i * 2; });
3
std::expected(P0323)
1enum class Status : uint8_t {
2 Ok,
3 connection_error,
4 no_authority,
5 format_error,
6};
7
8bool connected() {
9 return true;
10}
11
12bool has_authority() {
13 return false;
14}
15
16bool format() {
17 return false;
18}
19
20std::expected<std::string, Status> read_data() {
21 if (!connected())
22 return std::unexpected<Status> { Status::connection_error };
23 if (!has_authority())
24 return std::unexpected<Status> { Status::no_authority };
25 if (!format())
26 return std::unexpected<Status> { Status::format_error };
27
28 return {"my expected type"};
29 }
30
31
32int main() {
33 auto result = read_data();
34 if (result) {
35 std::cout << result.value() << "\n";
36 } else {
37 std::cout << "error code: " << (int)result.error() << "\n";
38 }
39}
这种方式无疑会简化错误处理的操作。
该特性目前在GCC 12,Clang 16(还未发布),MSVC v19.33已经实现。
4
Multidimensional Arrays(P2128)
这个特性用于访问多维数组,之前C++ operator[]只支持访问单个下标,无法访问多维数组。
因此要访问多维数组,以前的方式是:
重载operator(),于是能够以m(1, 2)来访问第1行第2个元素。但这种方式容易和函数调用产生混淆;
重载operator[],并以std::initializer_list作为参数,然后便能以m[{1, 2}]来访问元素。但这种方式看着别扭。
链式链接operator[],然后就能够以m[1][2]来访问元素。同样,看着别扭至极。
定义一个at()成员,然后通过at(1, 2)访问元素。同样不方便。
感谢该提案,在C++23,我们终于可以通过m[1, 2]这种方式来访问多维数组。
一个例子:
1template <class T, size_t R, size_t C>
2struct matrix {
3 T& operator[](const size_t r, const size_t c) noexcept {
4 return data_[r * C + c];
5 }
6
7 const T& operator[](const size_t r, const size_t c) const noexcept {
8 return data_[r * C + c];
9 }
10
11private:
12 std::array<T, R * C> data_;
13};
14
15
16int main() {
17 matrix<int, 2, 2> m;
18 m[0, 0] = 0;
19 m[0, 1] = 1;
20 m[1, 0] = 2;
21 m[1, 1] = 3;
22
23 for (auto i = 0; i < 2; ++i) {
24 for (auto j = 0; j < 2; ++j) {
25 std::cout << m[i, j] << ' ';
26 }
27 std::cout << std::endl;
28 }
29}
该特性目前在GCC 12和Clang 15以上版本已经支持。
5
if consteval(P1938)
1consteval auto bar(int m) {
2 return m * 6;
3}
4
5constexpr auto foo(int m) {
6 return bar(m);
7}
8
9
10int main() {
11 [[maybe_unused]] auto res = foo(42);
12}
1constexpr auto foo(int m) {
2 if (std::is_constant_evaluated()) {
3 return bar(m);
4 }
5 return 42;
6}
1constexpr auto foo(int m) {
2 if consteval {
3 return bar(m);
4 }
5 return 42;
6}
6
Formatted Output(P2093)
标准cout的设计非常糟糕,具体表现在:
可用性差,基本没有格式化能力;
会多次调用格式化I/0函数;
默认会同步标准C,性能低;
内容由参数交替组成,在多线程环境,内容会错乱显示;
二进制占用空间大;
……
随着Formatting Library加入C++20,已在fmt库中使用多年的fmt::print()加入标准也是顺理成章。
1#include <print>
2
3int main() {
4 const char* world = "world";
5 std::print("Hello {}", world); // doesn't print a newline
6 std::println("Hello {}", world); // print a newline
7}
----------------------------------------------------------
Benchmark Time CPU Iterations
----------------------------------------------------------
printf 87.0 ns 86.9 ns 7834009
ostream 255 ns 255 ns 2746434
print 78.4 ns 78.3 ns 9095989
print_cout 89.4 ns 89.4 ns 7702973
print_cout_sync 91.5 ns 91.4 ns 7903889
7
Formatting Ranges(P2286)
import std;
auto main() -> int {
std::vector vec { 1, 2, 3 };
std::print("{}\n", vec); // Output: [1, 2, 3]
}
1print("how you doing".split(" "))
2
3# Output:
4# ['how', 'you', 'doing']
1import java.util.Arrays;
2
3class Main {
4 public static void main(String args[]) {
5 System.out.println("how you doing".split(" "));
6 System.out.println(Arrays.toString("how you doing".split(" ")));
7 }
8}
9
10// Output:
11// [Ljava.lang.String;@2b2fa4f7
12// [how, you, doing]
1use itertools::Itertools;
2
3fn main() {
4 println!("{:?}", "How you doing".split(' '));
5 println!("[{}]", "How you doing".split(' ').format(", "));
6 println!("{:?}", "How you doing".split(' ').collect::<Vec<_>>());
7}
8
9// Output:
10// Split(SplitInternal { start: 0, end: 13, matcher: CharSearcher { haystack: "How you doing", finger: 0, finger_back: 13, needle: ' ', utf8_size: 1, utf8_encoded: [32, 0, 0, 0] }, allow_trailing_empty: true, finished: false })
11// [How, you, doing]
12// ["How", "you", "doing"]
1console.log('How you doing'.split(' '))
2
3// Output:
4// ["How", "you", "doing"]
1package main
2import "fmt"
3import "strings"
4
5func main() {
6 fmt.Println(strings.Split("How you doing", " "));
7}
8
9// Output:
10// [How you doing]
1fun main() {
2 println("How you doing".split(" "));
3}
4
5// Output:
6// [How, you, doing]
1int main() {
2 std::string_view contents {"How you doing"};
3
4 auto words = contents
5 | std::views::split(' ')
6 | std::views::transform([](auto&& str) {
7 return std::string_view(&*str.begin(), std::ranges::distance(str));
8 });
9
10 std::cout << "[";
11 char const* delim = "";
12 for (auto word : words) {
13 std::cout << delim;
14
15 std::cout << std::quoted(word);
16 delim = ", ";
17 }
18 std::cout << "]\n";
19}
20
21// Output:
22// ["How", "you", "doing"]
1int main() {
2 std::string_view contents {"How you doing"};
3
4 auto words = contents
5 | std::views::split(' ')
6 | std::views::transform([](auto&& str) {
7 return std::string_view(&*str.begin(), std::ranges::distance(str));
8 });
9
10 fmt::print("{}\n", words);
11 fmt::print("<<{}>>", fmt::join(words, "--"));
12
13}
14
15// Output:
16// ["How", "you", "doing"]
17// <<How--you--doing>>
因为views::split()返回的是一个subrange,因此需要将其转变成string_view,否则,输出将为:
1int main() {
2 std::string_view contents {"How you doing"};
3
4 auto words = contents | std::views::split(' ');
5
6 fmt::print("{}\n", words);
7 fmt::print("<<{}>>", fmt::join(words, "--"));
8
9}
10
11// Output:
12// [[H, o, w], [y, o, u], [d, o, i, n, g]]
13// <<['H', 'o', 'w']--['y', 'o', 'u']--['d', 'o', 'i', 'n', 'g']>>
总之,这个特性将极大简化Ranges的输出,是值得兴奋的特性之一。
该特性目前没有编译器支持。
7
import std(P2465)
C++20模块很难用的一个原因就是标准模块没有提供,因此这个特性的加入是自然趋势。
1import std;
2
3int main() {
4 std::print("Hello standard library modules!\n");
5}
性能对比:
如何你是混合C和C++,那可以使用std.compat module,所有的C函数和标准库函数都会包含进来。
目前基本没有编译器支持此特性。
8
out_ptr(P1132r8)
1// Before
2int old_c_api(int**);
3
4int main() {
5 auto up = std::make_unique<int>(5);
6
7 int* up_raw = up.release();
8 if (int ec = foreign_resetter(&up)) {
9 return ec;
10 }
11
12 up.reset(up_raw);
13}
14
15////////////////////////////////
16// After
17int old_c_api(int**);
18
19int main() {
20 auto up = std::make_unique<int>(5);
21
22 if (int ec = foreign_resetter(std::inout_ptr(up))) {
23 return ec;
24 }
25
26 // *up is still valid
27}
该特性目前在MSVC v19.30支持。
9
auto(x) decay copy(P0849)
该提案为auto又增加了两个新语法:auto(x)和auto{x}。两个作用一样,只是写法不同,都是为x创建一份拷贝。
为什么需要这么个东西?
1void bar(const auto&);
2
3void foo(const auto& param) {
4 auto copy = param;
5 bar(copy);
6}
1void foo(const auto& param) {
2 bar(std::decay_t<decltype(param)>{param});
3}
1void foo(const auto& param) {
2 bar(auto{param});
3}
1void pop_front_alike(auto& container) {
2 std::erase(container, container.front());
3}
4
5int main() {
6 std::vector fruits{ "apple", "apple", "cherry", "grape",
7 "apple", "papaya", "plum", "papaya", "cherry", "apple"};
8 pop_front_alike(fruits);
9
10 fmt::print("{}\n", fruits);
11}
12
13// Output:
14// ["cherry", "grape", "apple", "papaya", "plum", "papaya", "apple"]
请注意该程序的输出,是否如你所想的一样。若没有发现问题,请让我再提醒一下:pop_front_alike()要移除容器中所有跟第1个元素相同的元素。
["cherry", "grape", "papaya", "plum", "papaya", "cherry"]
1template<typename _ForwardIterator, typename _Predicate>
2_ForwardIterator
3 __remove_if(_ForwardIterator __first, _ForwardIterator __last,
4 _Predicate __pred)
5{
6 __first = std::__find_if(__first, __last, __pred);
7 if (__first == __last)
8 return __first;
9 _ForwardIterator __result = __first;
10 ++__first;
11 for (; __first != __last; ++__first)
12 if (!__pred(__first)) {
13 *__result = _GLIBCXX_MOVE(*__first);
14 ++__result;
15 }
16
17 return __result;
18}
19
20template<typename _Tp, typename _Alloc, typename _Up>
21 inline typename vector<_Tp, _Alloc>::size_type
22erase(vector<_Tp, _Alloc>& __cont, const _Up& __value)
23{
24 const auto __osz = __cont.size();
25 __cont.erase(std::remove(__cont.begin(), __cont.end(), __value),
26 __cont.end());
27 return __osz - __cont.size();
28}
std::remove()最终调用的是remove_if(),因此关键就在这个算法里面。这个算法每次会比较当前元素和欲移除元素,若不相等,则用当前元素覆盖当前__result迭代器的值,然后__result向后移一位。重复这个操作,最后全部有效元素就都跑到__result迭代器的前面去了。
问题出在哪里呢?欲移除元素始终指向首个元素,而它会随着元素覆盖操作被改变,因为它的类型为const T&。
此时,必须重新copy一份值,才能得到正确的结果。
void pop_front_alike(auto& container) {
auto copy = container.front();
std::erase(container, copy);
}
然而这种方式是非常反直觉的,一般来说这两种写法的效果应该是等价的。
auto copy(const auto& value) {
return value;
}
void pop_front_alike(auto& container) {
std::erase(container, copy(container.front()));
}
而auto{x}和auto(x),就相当于这个copy()函数,只不过它是内建到语言里面的而已。
10
Narrowing contextual conversions to bool
这个提案允许在static_assert和if constexpr中从整形转换为布尔类型。
Before | After |
if constexpr(bool(flags & Flags::Exec)) | if constexpr(flags & Flags::Exec) |
if constexpr(flags & Flags::Exec != 0) | if constexpr(flags & Flags::Exec) |
static_assert(N % 4 != 0); | static_assert(N % 4); |
static_assert(bool(N)); | static_assert(N); |
对于严格的C++编译器来说,以前在这种情境下int无法向下转换为bool,需要手动强制转换,C++23这一情况得到了改善。
目前在GCC 9和Clang 13以上版本支持该特性。
11
forward_like(P2445)
1auto callback = [m = get_message(), &scheduler](this auto&& self) -> bool {
2 return scheduler.submit(std::forward_like<decltype(self)>(m));
3};
4
5callback(); // retry(callback)
6std::move(callback)(); // try-or-fail(rvalue)
std::forward_like加入到了<utility>中,就是根据模板参数的值类别来转发参数。
如果closure type为左值,那么m将转发为左值;如果为右值,将转发为右值。
听说Clang 16和MSVC v19.34支持该特性,但都尚未发布。
12
#elifdef and #elifndef(P2334)
这两个预处理指令来自WG14(C的工作组),加入到了C23。C++为了兼容C,也将它们加入到了C++23。
也是一个完善工作。
#ifdef和#ifndef分别是#if defined()和#if !defined()的简写,而#elif defined()和#elif !defined()却并没有与之对应的简写指令。因此,C23使用#elifdef和#elifndef来补充这一遗漏。
总之,是两个非常简单的小特性。目前已在GCC 12和Clang 13得到支持。
13
#warning(P2437)
1#ifndef FOO
2#warning "FOO defined, performance might be limited"
3#endif
目前MSVC不支持该特性,其他主流编译器都支持。
14
constexpr std::unique_ptr(P2273R3)
1constexpr auto fun() {
2 auto p = std::make_unique<int>(4);
3 return *p;
4}
5
6int main() {
7 constexpr auto i = fun();
8 static_assert(4 == i);
9}
目前GCC 12和MSVC v19.33支持该特性。
15
improving string and string_view(P1679R3, P2166R1, P1989R2, P1072R10, P2251R1)
string和string_view也获得了一些增强,这里简单地说下。
1std::string str("dummy text");
2if (str.contains("dummy")) {
3 // do something
4}
1std::string s { nullptr }; // error!
2std::string_view sv { nullptr }; // error!
1int main() {
2 std::vector v { 'a', 'b', 'c' };
3
4 // Before
5 std::string_view sv(v.begin(), v.end());
6
7 // After
8 std::string_view sv23 { v };
9}
以前无法直接从Ranges构建std::string_view,而现在支持这种方式。
该特性在GCC 11,Clang 14,MSVC v19.30已经支持。
1template< class Operation >
2constexpr void resize_and_overwrite( size_type count, Operation op );
1int main() {
2 std::string s { "Food: " };
3
4 s.resize_and_overwrite(10, [](char* buf, int n) {
5 return std::find(buf, buf + n, ':') - buf;
6 });
7
8 std::cout << std::quoted(s) << '\n'; // "Food"
9}
主要是两个操作:改变大小和覆盖内容。第1个参数是新的大小,第2个参数是一个op,用于设置新的内容。
如果maxsize <= s.size(),删除最后的size()-maxsize个元素;
如果maxsize > s.size(),追加maxsize-size()个默认元素;
调用erase(begin() + op(data(), maxsize), end())。
1constexpr std::string_view fruits[] {"apple", "banana", "coconut", "date", "elderberry"};
2std::string s1 { "Food: " };
3
4s1.resize_and_overwrite(16, [sz = s1.size()](char* buf, std::size_t buf_size) {
5 const auto to_copy = std::min(buf_size - sz, fruits[1].size()); // 6
6 std::memcpy(buf + sz, fruits[1].data(), to_copy); // append "banana" to s1.
7 return sz + to_copy; // 6 + 6
8});
9
10std::cout << s1; // Food: banana
注意一下,maxsize是最大的可能大小,而op返回才是实际大小,因此逻辑的最后才有一个erase()操作,用于删除多余的大小。
这个特性在GCC 12,Clang 14,MSVC v19.31已经实现。
接着来看P2251,它更新了std::span和std::string_view的约束,从C++23开始,它们必须满足TriviallyCopyable Concept。
主流编译器都支持该特性。
最后来看P0448,其引入了一个新的头文件<spanstream>。
大家都知道,stringstream现在被广泛使用,可以将数据存储到string或vector当中,但这些容器当数据增长时会发生「挪窝」的行为,若是不想产生这个开销呢?
<spanstream>提供了一种选择,你可以指定固定大小的buffer,它不会重新分配内存,但要小心数据超出buffer大小,此时内存的所有权在程序员这边。
1#define ASSERT_EQUAL(a, b) assert(a == b)
2#define ASSERT(a) assert(a)
3
4int main() {
5 char input[] = "10 20 30";
6 std::ispanstream is{ std::span<char>{input} };
7 int i;
8
9 is >> i;
10 ASSERT_EQUAL(10,i);
11
12 is >> i;
13 ASSERT_EQUAL(20,i);
14
15 is >> i;
16 ASSERT_EQUAL(30,i);
17
18 is >> i;
19 ASSERT(!is);
20}
目前GCC 12和MSVC v19.31已支持该特性。
16
static operator()(P1169R4)
因为函数对象,Lambdas使用得越来越多,经常作为标准库的定制点使用。这种函数对象只有一个operator (),如果允许声明为static,则可以提高性能。
至于原理,大家可以回顾一下Deducing this那节的Pass this by value提高性能的原理。明白静态函数和非静态函数在重载决议中的区别,大概就能明白这点。
顺便一提,由于mutidimensional operator[]如今已经可以达到和operator()一样的效果,它也可以作为一种新的函数语法,你完全可以这样调用foo[],只是不太直观。因此,P2589也提议了static operator[]。
17
std::unreachable(P0627R6)
1void foo(int a) {
2 switch (a) {
3 case 1:
4 // do something
5 break;
6 case 2:
7 // do something
8 break;
9 default:
10 std::unreachable();
11 }
12}
13
14bool is_valid(int a) {
15 return a == 1 || a == 2;
16}
17
18int main() {
19 int a = 0;
20 while (!is_valid(a))
21 std::cin >> a;
22 foo(a);
23}
18
std::to_underlying(P1682R3)
同样位于<utility>,用于枚举到其潜在的类型,相当于以下代码的语法糖:
static_cast<std::underlying_type_t<Enum>>(e);
一个简单的例子就能看懂:
1void print_day(int a) {
2 fmt::print("{}\n", a);
3}
4
5enum class Day : std::uint8_t {
6 Monday = 1,
7 Tuesday,
8 Wednesday,
9 Thursday,
10 Friday,
11 Saturday,
12 Sunday
13};
14
15
16int main() {
17 // Before
18 print_day(static_cast<std::underlying_type_t<Day>>(Day::Monday));
19
20 // C++23
21 print_day(std::to_underlying(Day::Friday));
22}
该特性目前在GCC 11,Clang 13,MSVC v19.30已经实现。
19
std::byteswap(P1272R4)
位于<bit>,顾名思义,是关于位操作的。
同样,一个例子看懂:
1template <std::integral T>
2void print_hex(T v)
3{
4 for (std::size_t i = 0; i < sizeof(T); ++i, v >>= 8)
5 {
6 fmt::print("{:02X} ", static_cast<unsigned>(T(0xFF) & v));
7 }
8 std::cout << '\n';
9 }
10
11int main()
12{
13 unsigned char a = 0xBA;
14 print_hex(a); // BA
15 print_hex(std::byteswap(a)); // BA
16 unsigned short b = 0xBAAD;
17 print_hex(b); // AD BA
18 print_hex(std::byteswap(b)); // BA AD
19 int c = 0xBAADF00D;
20 print_hex(c); // 0D F0 AD BA
21 print_hex(std::byteswap(c)); // BA AD F0 0D
22 long long d = 0xBAADF00DBAADC0FE;
23 print_hex(d); // FE C0 AD BA 0D F0 AD BA
24 print_hex(std::byteswap(d)); // BA AD F0 0D BA AD C0 FE
25}
该特性目前在GCC 12,Clang 14和MSVC v19.31已经支持。
20
std::stacktrace(P0881R7, P2301R1)
一个小例子:
1void foo() {
2 auto trace = std::stacktrace::current();
3 std::cout << std::to_string(trace) << '\n';
4}
5
6int main() {
7 foo();
8}
输出如下。
0# foo() at /app/example.cpp:5
1# at /app/example.cpp:10
2# at :0
3# at :0
4#
注意,目前GCC 12.1和MSVC v19.34支持该特性,GCC 编译时要加上-lstdc++_libbacktrace参数。
std::stacktrace是std::basic_stacktrace使用默认分配器时的别名,定义为:
using stacktrace = std::basic_stacktrace<std::allocator<std::stacktrace_entry>>;
而P2301,则是为其添加了PMR版本的别名,定义为:
namespace pmr {
using stacktrace =
std::basic_stacktrace<std::pmr::polymorphic_allocator<std::stacktrace_entry>>;
}
于是使用起来就会方便一些。
1// Before
2char buffer[1024];
3
4std::pmr::monotonic_buffer_resource pool{
5 std::data(buffer), std::size(buffer)};
6
7std::basic_stacktrace<
8 std::pmr::polymorphic_allocator<std::stacktrace_entry>>
9 trace{&pool};
10
11// After
12char buffer[1024];
13
14std::pmr::monotonic_buffer_resource pool{
15 std::data(buffer), std::size(buffer)};
16
17std::pmr::stacktrace trace{&pool};
这个特性到时再单独写篇文章,在此不细论。
21
Attributes(P1774R8, P2173R1, P2156R1)
Attributes在C++23也有一些改变。
首先,P1774新增了一个Attribute [[assume]],其实在很多编译器早已存在相应的特性,例如__assume()(MSVC, ICC),__builtin_assume()(Clang)。GCC没有相关特性,所以它也是最早实现标准[[assume]]的,目前就GCC 13支持该特性(等四月发布,该版本对Rangs的支持也很完善)。
1#if defined(__clang__)
2 #define ASSUME(expr) __builtin_assume(expr)
3#elif defined(__GNUC__) && !defined(__ICC)
4 #define ASSUME(expr) if (expr) {} else { __builtin_unreachable(); }
5#elif defined(_MSC_VER) || defined(__ICC)
6 #define ASSUME(expr) __assume(expr)
7#endif
1void limiter(float* data, size_t size) {
2 ASSUME(size > 0);
3 ASSUME(size % 32 == 0);
4
5 for (size_t i = 0; i < size; ++i) {
6 ASSUME(std::isfinite(data[i]));
7 data[i] = std::clamp(data[i], -1.0f, 1.0f);
8 }
9}
第一个是假设size永不为0,总是正数;第二个告诉编译器size总是32的倍数;第三个表明数据不是NaN或无限小数。
这些假设不会被评估,也不会被检查,编译器假设其为真,依此优化代码。若是假设为假,可能会产生UB。
其次,P2173使得可以在Lambda表达式上使用Attributes,一个例子:
1// Any attribute so specified does not appertain to the function
2// call operator or operator template itself, but its type.
3auto lam = [][[nodiscard]] ->int { return 42; };
4
5int main()
6{
7 lam();
8}
9
10// Output:
11// <source>: In function 'int main()':
12// <source>:12:8: warning: ignoring return value of '<lambda()>', declared with attribute 'nodiscard' [-Wunused-result]
13// 12 | lam();
14// | ~~~^~
15// <source>:8:12: note: declared here
16// 8 | auto lam = [][[nodiscard]] ->int { return 42; };
17// | ^
注意,Attributes属于closure type,而不属于operator ()。
因此,有些Attributes不能使用,比如[[noreturn]],它表明函数的控制流不会返回到调用方,而对于Lambda函数是会返回的。
除此之外,此处我还展示了C++的另一个Lambda特性。
在C++23之前,最简单的Lambda表达式为[](){},而到了C++23,则是[]{},可以省略无参时的括号,这得感谢P1102。
早在GCC 9就支持Attributes Lambda,Clang 13如今也支持。
最后来看P2156,它移除了重复Attributes的限制。
1// Not allow
2[[nodiscard, nodiscard]] auto foo() {
3 return 42;
4}
5
6// Allowed
7[[nodiscard]][[nodiscard]] auto foo() {
8 return 42;
9}
为了保证一致性,去除此限制,使得标准更简单。
什么时候会出现重复Attributes,看论文怎么说:
During this discussion, it was brought up that
the duplication across attribute-specifiers are to support cases where macros are used to conditionally add attributes to an
attribute-specifier-seq, however it is rare for macros to be used to generate attributes within the same attribute-list. Thus,
removing the limitation for that reason is unnecessary.
在基于宏生成的时候可能会出现重复Attributes,因此允许第二种方式;宏生成很少使用第一种形式,因此标准限制了这种情况。但这却并没有让标准变得更简单。因此,最终移除了该限制。
目前使用GCC 11,Clang 13以上两种形式的结果将保持一致。
22
Lambdas(P1102R2, P2036R3, P2173R1)
Lambdas表达式在C++23也再次迎来了一些新特性。
像是支持Attributes,可以省略(),这在Attributes这一节已经介绍过,不再赘述。
另一个新特性是P2036提的,接下来主要说说这个。
1double j = 42.0;
2// ...
3auto counter = [j = 0]() mutable -> decltype(j) {
4 return j++;
5};
counter最终的类型是什么?是int吗?还是double?其实是double。
无论捕获列表当中存在什么值,trailing return type的Name Lookup都不会查找到它。
1auto counter = [j=0]() mutable -> decltype(j) {
2 return j++;
3};
4
5// Output:
6// <source>:6:44: error: use of undeclared identifier 'j'
7// auto counter = [j=0]() mutable -> decltype(j) {
8// ^
因为对于trailing return type来说,根本就看不见捕获列表中的j。
1template <typename T> int bar(int&, T&&); // #1
2template <typename T> void bar(int const&, T&&); // #2
3
4int i;
5auto f = [=](auto&& x) -> decltype(bar(i, x)) {
6 return bar(i, x);
7}
8
9f(42); // error
23
Literal suffixes for (signed) size_t(P0330R8)
这个特性为std::size_t增加了后缀uz,为signed std::size_t加了后缀z。
有什么用呢?看个例子:
1#include <vector>
2
3int main() {
4 std::vector<int> v{0, 1, 2, 3};
5 for (auto i = 0u, s = v.size(); i < s; ++i) {
6 /* use both i and v[i] */
7 }
8}
这代码在32 bit平台编译能够通过,而放到64 bit平台编译,则会出现错误:
1<source>(5): error C3538: in a declarator-list 'auto' must always deduce to the same type
2<source>(5): note: could be 'unsigned int'
3<source>(5): note: or 'unsigned __int64'
在32 bit平台上,i被推导为unsigned int,v.size()返回的类型为size_t。而size_t在32 bit上为unsigned int,而在64 bit上为unsigned long long。(in MSVC)
因此,同样的代码,从32 bit切换到64 bit时就会出现错误。
而通过新增的后缀,则可以保证这个代码在任何平台上都能有相同的结果。
1#include <vector>
2
3int main() {
4 std::vector<int> v{0, 1, 2, 3};
5 for (auto i = 0uz, s = v.size(); i < s; ++i) {
6 /* use both i and v[i] */
7 }
8}
如此一来就解决了这个问题。
目前GCC 11和Clang 13支持该特性。
24
std::mdspan(P0009r18)
1int main()
2{
3 std::vector v = {1,2,3,4,5,6,7,8,9,10,11,12};
4
5 // View data as contiguous memory representing 2 rows of 6 ints each
6 auto ms2 = std::experimental::mdspan(v.data(), 2, 6);
7 // View the same data as a 3D array 2 x 3 x 2
8 auto ms3 = std::experimental::mdspan(v.data(), 2, 3, 2);
9
10 // write data using 2D view
11 for(size_t i=0; i != ms2.extent(0); i++)
12 for(size_t j=0; j != ms2.extent(1); j++)
13 ms2[i, j] = i*1000 + j;
14
15 // read back using 3D view
16 for(size_t i=0; i != ms3.extent(0); i++)
17 {
18 fmt::print("slice @ i = {}\n", i);
19 for(size_t j=0; j != ms3.extent(1); j++)
20 {
21 for(size_t k=0; k != ms3.extent(2); k++)
22 fmt::print("{} ", ms3[i, j, k]);
23 fmt::print("\n");
24 }
25 }
26}
1slice @ i = 0
20 1
32 3
44 5
5slice @ i = 1
61000 1001
71002 1003
81004 1005
25
flat_map, flat_set(P0429R9, P1222R4)
C++23多了flat version的map和set:
flat_map
flat_set
flat_multimap
flat_multiset
过去的容器,有的使用二叉树,有的使用哈希表,而flat版本的使用的连续序列的容器,更像是容器的适配器。
无非就是时间或空间复杂度的均衡,目前没有具体测试,也没有编译器支持,暂不深究。
26
总结
本篇已经够长了,C++23比较有用的特性基本都包含进来了。
其中的另一个重要更新Ranges并没有包含,是因为之前已经单独写过单独文章了。参考C++23 | Ranges的修复与完善
读至此,大家应该已经感觉到C++23在于完善,而不在于增加。没有什么全新的东西,也没什么太大的特性,那些就得等到C++26了。
很多特性在这篇概述就已经讲清了原理,而有几个特性还是值得深挖一下,后面单独再出文章。还有少数芝麻大小的特性本文没有包含,必要的话也留到后面单独再介绍。
大家喜欢哪些C++23特性?
See more