C++23特性概览

新年伊始,要说什么选题最合适,那无疑是c++23了。
23是个小版本,主要在于「完善」二字,而非「新增」。因此,值得单独拿出来写篇文章的特性其实并不多,大多特性都是些琐碎小点,三言两语便可讲清。
本篇包含绝大多数c++23特性,难度三星就表示只会介绍基本用法,但有些特性的原理也会深入讲讲。
1deducing this(p0847)
deducing this是c++23中最主要的特性之一。msvc在去年3月份就已支持该特性,可以在v19.32之后的版本使用。
为什么我们需要这个特性?
大家知道,成员函数都有一个隐式对象参数,对于非静态成员函数,这个隐式对象参数就是this指针;而对于静态成员函数,这个隐式对象参数被定义为可以匹配任何参数,这仅仅是为了保证重载决议可以正常运行。
deducing this所做的事就是提供一种将非静态成员函数的「隐式对象参数」变为「显式对象参数」的方式。为何只针对非静态成员函数呢?因为静态成员函数并没有this指针,隐式对象参数并不能和this指针划等号,静态函数拥有隐式对象参数只是保证重载决议能够正常运行而已,这个参数没有其他用处。
于是,现在便有两种写法编写非静态成员函数。
1struct s_implicit {2    void foo() {}3};45struct 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// after10struct s_explicit {11    int data_;1213    template 14    auto&& foo(this self& self) {15        return std::forward(self).data_;16    }17};原本你也许得为同一个成员函数编写各种版本的修饰,比如&, const&, &&, const &&,其逻辑并无太大变化,完全是重复的机械式操作。如今借助deducing this,你只需编写一个版本即可。 这里使用了模板形式的参数,通常来说,建议是使用self作为显式对象参数的名称,顾名思义的同时又能和其他语言保持一致性。 该特性还有许多使用场景,同时也是一种新的定制点表示方式。 比如,借助deducing this,可以实现递归lambdas。1int main() {2    auto gcd = [](this auto self, int a, int b) -> int {3        return b == 0 ? a : self(b, a % b);4    };56    std::cout << gcd(20, 30) << ;7}这使得lambda函数再次得到增强。 又比如,借助deducing this,可以简化crtp。 1//// before 2// crtp 3template  4struct base { 5    void foo() { 6        auto& self = *static_cast(this); 7        self.bar(); 8    } 9};1011struct derived : base {12    void bar() const {13        std::cout << crtp derived;14    }15};1617////////////////////////////////////////////18//// after19// deducing this20struct base {21    template 22    void foo(this self& self) {23        self.bar();24    }25};2627struct derived : base {28    void bar() const {29        std::cout << deducing this derived;30    }31};这种新的方式实现crtp,可以省去cr,甚至是t,要更加自然,更加清晰。 这也是一种新的定制点方式,稍微举个简单点的例子: 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 mylib1011namespace 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 userspace1920int main() {21    using userspace::m;22    m m;23    m.abstract_interface(4);24}  这种方式依旧属于静态多态的方式,但代码更加清晰、无侵入,并支持显式opt-in,是一种值得使用的方式。 定制点并非一个简单的概念,若是看不懂以上例子,跳过便是。 下面再来看其他的使用场景。 deducing this还可以用来解决根据closure类型完美转发lambda捕获参数的问题。 亦即,如果lambda函数的类型为左值,那么捕获的参数就以左值转发;如果为右值,那么就以右值转发。下面是一个例子: 1#include  2#include  3#include  // 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::value << ;13        std::cout << std::is_rvalue_reference::value < bool {21        return scheduler.submit(std::forward_like(m));22    };23    callback(); // retry(callback)24    std::move(callback)(); // try-or-fail(rvalue)25}2627// output:28// true29// false30// false31// true  若是没有deducing this,那么将无法简单地完成这个操作。 另一个用处是可以将this以值形式传递,对于小对象来说,可以提高性能。 一个例子: 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}1112// implicit this pointer生成的汇编代码:13// sub     rsp, 40                             ; 00000028h14// lea     rcx, qword ptr s$[rsp]15// mov     dword ptr s$[rsp], 42               ; 0000002ah16// call    int s::foo(void)                    ; s::foo17// add     rsp, 40                             ; 00000028h18// ret     01920// pass this by value生成的汇编代码:21// mov     ecx, 42                             ; 0000002ah22// jmp     static int s::foo(this s)           ; s::foo  对于隐式的this指针,生成的汇编代码需要先分配栈空间,保存this指针到rcx寄存器中,再将42赋值到data_中,然后调用foo(),最后平栈。 而以值形式传递this,则无需那些操作,因为值传递的this不会影响s变量,中间的步骤都可以被优化掉,也不再需要分配和平栈操作,所以可以直接将42保存到寄存器当中,再jmp到foo()处执行。   deducing this是个单独就可写篇四五星难度文章的特性,用处很多,值得深入探索的地方也很多,所以即便是概述这部分也写得比较多。  
2monadic std::optional(p0798r8)
p0798提议为std::optional增加三个新的成员:map(), and_then()和or_else()。   功能分别为:
map:对optional的值应用一个函数,返回optional中wrapped的结果。若是optional中没有值,返回一个空的optional;
and_then:组合使用返回optional的函数;
or_else:若是有值,返回optional;若是无值,则调用传入的函数,在此可以处理错误。
  在r2中map()被重命名为transform(),因此实际新增的三个函数为transform(),and_then()和or_else()。   这些函数主要是避免手动检查optional值是否有效,比如:  
1// before2if (opt_string) {3   std::optional i = stoi(*opt_string);4}56// after7std::optional i = opt_string.and_then(stoi);    一个使用的小例子:  1// chain a series of functions until there's an error2std::optional opt_string(10);3std::optional i = opt_string4                        .and_then(std::stoi)5                        .transform([](auto i) { return i * 2; });  错误的情况:  1// fails, transform not called, j == nullopt2std::optional opt_string_bad(abcd);3std::optional j = opt_string_bad4                        .and_then(std::stoi)5                        .transform([](auto i) { return i * 2; });    目前gcc 12,clang 14,msvc v19.32已经支持该特性。  
3std::expected(p0323)
该特性用于解决错误处理的问题,增加了一个新的头文件。 错误处理的逻辑关系为条件关系,若正确,则执行a逻辑;若失败,则执行b逻辑,并需要知道确切的错误信息,才能对症下药。 当前的常用方式是通过错误码或异常,但使用起来还是多有不便。 std::expected表示期望,算是std::variant和std::optional的结合,它要么保留t(期望的类型),要么保留e(错误的类型),它的接口又和std::optional相似。 一个简单的例子:
1enum class status : uint8_t { 2    ok, 3    connection_error, 4    no_authority, 5    format_error, 6}; 7 8bool connected() { 9    return true;10}1112bool has_authority() {13    return false;14}1516bool format() {17    return false;18}1920std::expected read_data() {21    if (!connected())22        return std::unexpected { status::connection_error };23    if (!has_authority())24        return std::unexpected { status::no_authority };25    if (!format())26        return std::unexpected { status::format_error };2728    return {my expected type};29 }303132int main() {33    auto result = read_data();34    if (result) {35        std::cout << result.value() << ;36    } else {37        std::cout << error code:  << (int)result.error() << ; 38    }39}  
这种方式无疑会简化错误处理的操作。
该特性目前在gcc 12,clang 16(还未发布),msvc v19.33已经实现。
4multidimensional 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  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    }1011private:12    std::array data_;13};141516int main() {17    matrix m;18    m[0, 0] = 0;19    m[0, 1] = 1;20    m[1, 0] = 2;21    m[1, 1] = 3;2223    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 < int {    std::vector vec { 1, 2, 3 };    std::print({}, vec); // output: [1, 2, 3]}这意味着再也不用迭代来输出ranges了。 这是非常有必要的,考虑一个简单的需求:文本分割。 python的实现:1print(how you doing.split( ))23# output:4# ['how', 'you', 'doing']java的实现: 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} 910// output:11// [ljava.lang.string;@2b2fa4f712// [how, you, doing]rust的实现: 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::()); 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]js的实现:1console.log('how you doing'.split(' '))23// output:4// [how, you, doing]go的实现: 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]kotlin的实现:1fun main() {2    println(how you doing.split( ));3}45// output:6// [how, you, doing]c++的实现: 1int main() { 2    std::string_view contents {how you doing}; 3 4    auto words = contents 5                | std::split(' ') 6                | std::transform([](auto&& str) { 7                    return std::string_view(&*str.begin(), std::distance(str));  8                }); 910    std::cout << [;11    char const* delim = ;12    for (auto word : words) {13        std::cout << delim;1415        std::cout << std::quoted(word);16        delim = , ;17    }18    std::cout << ];19}2021// output:22// [how, you, doing]借助fmt,可以简化代码: 1int main() { 2    std::string_view contents {how you doing}; 3 4    auto words = contents 5                | std::split(' ') 6                | std::transform([](auto&& str) { 7                    return std::string_view(&*str.begin(), std::distance(str));  8                }); 910    fmt::print({}, words); 11    fmt::print(, fmt::join(words, --));1213}1415// output:16// [how, you, doing]17//   
因为views::split()返回的是一个subrange,因此需要将其转变成string_view,否则,输出将为:
1int main() { 2    std::string_view contents {how you doing}; 3 4    auto words = contents | std::split(' '); 5 6    fmt::print({}, words); 7    fmt::print(, fmt::join(words, --)); 8 9}1011// output:12// [[h, o, w], [y, o, u], [d, o, i, n, g]]13//   
总之,这个特性将极大简化ranges的输出,是值得兴奋的特性之一。
该特性目前没有编译器支持。
7import std(p2465)
c++20模块很难用的一个原因就是标准模块没有提供,因此这个特性的加入是自然趋势。
现在,可以写出这样的代码:
1import std;23int main() {4    std::print(hello standard library modules!);5}  
性能对比:
如何你是混合c和c++,那可以使用std.compat module,所有的c函数和标准库函数都会包含进来。
目前基本没有编译器支持此特性。
8out_ptr(p1132r8)
23新增了两个对于指针的抽象类型,std::out_ptr_t和std::inout_ptr_t,两个新的函数std::out_ptr()和std::inout_ptr()分别返回这两个类型。   主要是在和c api交互时使用的,一个例子对比一下:    
1// before 2int old_c_api(int**); 3 4int main() { 5    auto up = std::make_unique(5); 6 7    int* up_raw = up.release(); 8    if (int ec = foreign_resetter(&up)) { 9        return ec;10    }1112    up.reset(up_raw);13}1415////////////////////////////////16// after17int old_c_api(int**);1819int main() {20    auto up = std::make_unique(5);2122    if (int ec = foreign_resetter(std::inout_ptr(up))) {23        return ec;24    }2526    // *up is still valid27}  
该特性目前在msvc v19.30支持。
9auto(x) decay copy(p0849)
该提案为auto又增加了两个新语法:auto(x)和auto{x}。两个作用一样,只是写法不同,都是为x创建一份拷贝。
为什么需要这么个东西?
看一个例子:
1void bar(const auto&);23void foo(const auto& param) {4    auto copy = param;5    bar(copy);6}foo()中调用bar(),希望传递一份param的拷贝,则我们需要单独多声明一个临时变量。或是这样:1void foo(const auto& param) {2    bar(std::decay_t{param});3}这种方式需要手动去除多余的修饰,只留下t,要更加麻烦。 auto(x)就是内建的decay copy,现在可以直接这样写: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); 910    fmt::print({}, fruits);11}1213// output:14// [cherry, grape, apple, papaya, plum, papaya, apple]  
请注意该程序的输出,是否如你所想的一样。若没有发现问题,请让我再提醒一下:pop_front_alike()要移除容器中所有跟第1个元素相同的元素。
因此,理想的结果应该为:
[cherry, grape, papaya, plum, papaya, cherry]是哪里出了问题呢?让我们来看看gcc std::erase()的实现: 1template 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        }1617    return __result;18}1920template21    inline typename vector::size_type22erase(vector& __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);}  
然而这种方式是非常反直觉的,一般来说这两种写法的效果应该是等价的。
我们将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()函数,只不过它是内建到语言里面的而已。
10narrowing 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以上版本支持该特性。
11forward_like(p2445)
这个在deducing this那节已经使用过了,是同一个作者。 使用情境让我们回顾一下这个例子:
1auto callback = [m = get_message(), &scheduler](this auto&& self) -> bool {2    return scheduler.submit(std::forward_like(m));3};45callback();            // retry(callback)6std::move(callback)(); // try-or-fail(rvalue)  
std::forward_like加入到了中,就是根据模板参数的值类别来转发参数。
如果closure type为左值,那么m将转发为左值;如果为右值,将转发为右值。
听说clang 16和msvc v19.34支持该特性,但都尚未发布。
12#eifdef and #eifndef(p2334)
这两个预处理指令来自wg14(c的工作组),加入到了c23。c++为了兼容c,也将它们加入到了c++23。
也是一个完善工作。
#ifdef和#ifndef分别是#if defined()和#if !defined()的简写,而#elif defined()和#elif !defined()却并没有与之对应的简写指令。因此,c23使用#eifdef和#eifndef来补充这一遗漏。
总之,是两个非常简单的小特性。目前已在gcc 12和clang 13得到支持。
13#warning(p2437)
#warning是主流编译器都会支持的一个特性,最终倒逼c23和c++23也加入了进来。 这个小特性可以用来产生警告信息,与#error不同,它并不会停止翻译。 用法很简单:
1#ifndef foo2#warning foo defined, performance might be limited3#endif  
目前msvc不支持该特性,其他主流编译器都支持。
14constexpr std::unique_ptr(p2273r3)
std::unique_ptr也支持编译期计算了,一个小例子:
1constexpr auto fun() {2    auto p = std::make_unique(4);3    return *p;4}56int main() {7    constexpr auto i = fun();8    static_assert(4 == i);9}  
目前gcc 12和msvc v19.33支持该特性。
15improving string and string_view(p1679r3, p2166r1, p1989r2, p1072r10,  p2251r1)
string和string_view也获得了一些增强,这里简单地说下。
p1679为二者增加了一个contain()函数,小例子:
1std::string str(dummy text);2if (str.contains(dummy)) {3    // do something4}目前gcc 11,clang 12,msvc v19.30支持该特性。 p2166使得它们从nullptr构建不再产生ub,而是直接编译失败。1std::string s { nullptr };       // error!2std::string_view sv { nullptr }; // error!目前gcc 12,clang 13,msvc v19.30支持该特性。 p1989是针对std::string_view的,一个小例子搞定:1int main() {2    std::vector v { 'a', 'b', 'c' };34    // before5    std::string_view sv(v.begin(), v.end());67    // after8    std::string_view sv23 { v };9}  
以前无法直接从ranges构建std::string_view,而现在支持这种方式。
该特性在gcc 11,clang 14,msvc v19.30已经支持。
p1072为string新增了一个成员函数:
1template2constexpr void resize_and_overwrite( size_type count, operation op );可以通过提案中的一个示例来理解:1int main() {2    std::string s { food:  };34    s.resize_and_overwrite(10, [](char* buf, int n) {5        return std::find(buf, buf + n, ':') - buf;6    });78    std::cout << std::quoted(s) << ''; // food9}  
主要是两个操作:改变大小和覆盖内容。第1个参数是新的大小,第2个参数是一个op,用于设置新的内容。
然后的逻辑是:
如果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}); 910std::cout <> i;10    assert_equal(10,i);1112    is >> i;13    assert_equal(20,i);1415    is >> i;16    assert_equal(30,i);1718    is >> i;19    assert(!is);20}  
目前gcc 12和msvc v19.31已支持该特性。
16static operator()(p1169r4)
因为函数对象,lambdas使用得越来越多,经常作为标准库的定制点使用。这种函数对象只有一个operator (),如果允许声明为static,则可以提高性能。
至于原理,大家可以回顾一下deducing this那节的pass this by value提高性能的原理。明白静态函数和非静态函数在重载决议中的区别,大概就能明白这点。
顺便一提,由于mutidimensional operator[]如今已经可以达到和operator()一样的效果,它也可以作为一种新的函数语法,你完全可以这样调用foo[],只是不太直观。因此,p2589也提议了static operator[]。
17std::unreachable(p0627r6)
当我们知道某个位置是不可能执行到,而编译器不知道时,使用std::unreachalbe可以告诉编译器,从而避免没必要的运行期检查。 一个简单的例子:
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}1314bool is_valid(int a) {15    return a == 1 || a == 2;16}1718int main() {19    int a = 0;20    while (!is_valid(a))21        std::cin >> a;22    foo(a);23}该特性位于,在gcc 12,clang 15和msvc v19.32已经支持。  
18std::to_underlying(p1682r3)
同样位于,用于枚举到其潜在的类型,相当于以下代码的语法糖:
static_cast(e);  
一个简单的例子就能看懂:
1void print_day(int a) { 2    fmt::print({}, a); 3} 4 5enum class day : std::uint8_t { 6    monday = 1, 7    tuesday, 8    wednesday, 9    thursday,10    friday,11    saturday,12    sunday13};141516int main() {17    // before18    print_day(static_cast(day::monday));1920    // c++2321    print_day(std::friday));22}的确很简单吧!  
该特性目前在gcc 11,clang 13,msvc v19.30已经实现。
19std::byteswap(p1272r4)
位于,顾名思义,是关于位操作的。
同样,一个例子看懂:
1template  2void print_hex(t v) 3{ 4    for (std::size_t i = 0; i >= 8) 5    { 6        fmt::print({:02x} , static_cast(t(0xff) & v)); 7    }    8    std::cout << ''; 9    }1011int main()12{13    unsigned char a = 0xba;14    print_hex(a);                     // ba15    print_hex(std::byteswap(a));      // ba16    unsigned short b = 0xbaad;17    print_hex(b);                     // ad ba18    print_hex(std::byteswap(b));      // ba ad19    int c = 0xbaadf00d;20    print_hex(c);                     // 0d f0 ad ba21    print_hex(std::byteswap(c));      // ba ad f0 0d22    long long d = 0xbaadf00dbaadc0fe;23    print_hex(d);                     // fe c0 ad ba 0d f0 ad ba24    print_hex(std::byteswap(d));      // ba ad f0 0d ba ad c0 fe25}可以看到,其作用是逆转整型的字节序。当需要在两个不同的系统传输数据,它们使用不同的字节序时(大端小端),这个工具就会很有用。  
该特性目前在gcc 12,clang 14和msvc v19.31已经支持。
20std::stacktrace(p0881r7, p2301r1)
位于,可以让我们捕获调用栈的信息,从而知道哪个函数调用了当前函数,哪个调用引发了异常,以更好地定位错误。
一个小例子:
1void foo() {2    auto trace = std::current();3    std::cout << std::to_string(trace) << '';4}56int main() {7    foo();8}  
输出如下。
0# foo() at /app/example.cpp:51#      at /app/example.cpp:102#      at :03#      at :04#   
注意,目前gcc 12.1和msvc v19.34支持该特性,gcc 编译时要加上-lstdc++_libbacktrace参数。
std::stacktrace是std::basic_stacktrace使用默认分配器时的别名,定义为:
using stacktrace = std::basic_stacktrace;  
而p2301,则是为其添加了pmr版本的别名,定义为:
namespace pmr {using stacktrace =    std::basic_stacktrace;}  
于是使用起来就会方便一些。
1// before 2char buffer[1024]; 3 4std::monotonic_buffer_resource pool{ 5    std::data(buffer), std::size(buffer)}; 6 7std::basic_stacktrace 9    trace{&pool};1011// after12char buffer[1024];1314std::monotonic_buffer_resource pool{15    std::data(buffer), std::size(buffer)};1617std::stacktrace trace{&pool};  
这个特性到时再单独写篇文章,在此不细论。
21attributes(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);45    for (size_t i = 0; i int { return 42; }; 4 5int main() 6{ 7    lam(); 8} 910// output:11// : in function 'int main()':12// 8: warning: ignoring return value of '', declared with attribute 'nodiscard' [-wunused-result]13//    12 |     lam();14//       |     ~~~^~15// 12: note: declared here16//     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的限制。
简单来说,两种重复attributes的语法评判不一致。例子:
1// not allow2[[nodiscard, nodiscard]] auto foo() {3    return 42;4}56// allowed7[[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以上两种形式的结果将保持一致。
22lambdas(p1102r2, p2036r3, p2173r1)
lambdas表达式在c++23也再次迎来了一些新特性。
像是支持attributes,可以省略(),这在attributes这一节已经介绍过,不再赘述。
另一个新特性是p2036提的,接下来主要说说这个。
这个特性改变了trailing return types的name lookup规则,为什么?让我们来看一个例子。
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};45// output:6// 44: error: use of undeclared identifier 'j'7// auto counter = [j=0]() mutable -> decltype(j) {8//                                            ^  
因为对于trailing return type来说,根本就看不见捕获列表中的j。
以下例子能够更清晰地展示这个错误:
1template  int bar(int&, t&&);        // #12template  void bar(int const&, t&&); // #234int i;5auto f = [=](auto&& x) -> decltype(bar(i, x)) {6    return bar(i, x);7}89f(42); // error在c++23,trailing return types的name lookup规则变为:在外部查找之前,先查找捕获列表,从而解决这个问题。 目前没有任何编译器支持该特性。  
23literal suffixes for (signed) size_t(p0330r8)
这个特性为std::size_t增加了后缀uz,为signed std::size_t加了后缀z。
有什么用呢?看个例子:
1#include 23int main() {4  std::vector 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(5): error c3538: in a declarator-list 'auto' must always deduce to the same type2(5): note: could be 'unsigned int'3(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 23int main() {4    std::vector 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支持该特性。
24std::mdspan(p0009r18)
std::mdspan是std::span的多维版本,因此它是一个多维views。   看一个例子,简单了解其用法。  
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::mdspan(v.data(), 2, 6); 7  // view the same data as a 3d array 2 x 3 x 2 8  auto ms3 = std::mdspan(v.data(), 2, 3, 2); 910  // write data using 2d view11  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;1415  // read back using 3d view16  for(size_t i=0; i != ms3.extent(0); i++)17  {18    fmt::print(slice @ i = {}, 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();24    }25  }26}  目前没有编译器支持该特性,使用的是https://raw.githubusercontent.com/kokkos/mdspan/single-header/mdspan.hpp实现的版本,所以在experimental下面。   ms2是将数据以二维形式访问,ms3则以三维访问,views可以改变原有数据,因此最终遍历的结果为:  1slice @ i = 020 1 32 3 44 5 5slice @ i = 161000 1001 71002 1003 81004 1005   这个特性值得剖析下其设计,这里不再深究,后面单独出一篇文章。    
25flat_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在于完善,而不在于增加。没有什么全新的东西,也没什么太大的特性,那些就得等到c++26了。
大家喜欢哪些c++23特性?


拆焊贴片式集成电路的方法
海外服务器更换需要注意哪些方面的问题
从投资回收期数据分析LED照明替换路线图
LilyPad Arduino 328的使用教程
用光电隔离耦合控制交流负载电路
C++23特性概览
智能手机国内市场格局已定,印度市场和欧洲市场成为国产手机的海外主战场
TD多载波技术取得重大进展
华为PK三星 首发打孔屏智能手机
大数据怎样在5G运维管理上得到更好的应用?
联发科推出Helio P95处理器,GPU基准测试分数上提高10%
探讨5G网络的能力开放
基于物通博联工业数据采集网关的工厂数据采集方案
如何识别工控主板该不该返修,识别方法的介绍
AFG-X25系列USB任意波信号发生器的性能特点及应用范围
小米推出小米CC9 Pro等多款产品,首款量产的1亿像素摄像头手机
宜科定制化软硬件集成为知名汽车零部件厂商打造座椅装配线
c语言指针用法简单举例 C51的指针概述
观海微GH8555BL+BOE9.0(AV090HDQ-N19)/ Boe10.1(AV101HDQ-N19)搭配原理及代码
月度开发者Aryan Behzadi 利用 Qualcomm 技术使AR创意变成现实