C++23新特性个人总结

2,644 阅读24分钟

C++23新特性

推荐编译器版本:GCC 13

__cplusplus:202302L

编译选项:-std=c++23或-std=gnu++2b

1 关键字

1.1 consteval

编译器版本:GCC 12

文档链接P1938R3: if consteval

新增版本:C++20,可查看C++20新特性进行回顾

扩展适用范围,新增支持if表达式

前版本存在的问题:

#include <type_traits>// 立即函数
consteval int f(int i) 
{ 
    return i; 
}
​
constexpr int g(int i) 
{
    // 标准库中,is_constant_evaluated函数声明为constexpr
    if constexpr (std::is_constant_evaluated()) 
    {
        // 当前为常量语境时执行
        return f(i) + 1; // error,g()函数并非常量表达式
    } 
    else 
    {
        return 42;
    }
}
​
// 立即函数
consteval int h(int i) 
{
    return f(i) + 1;
}

在语义角度看,既然if constexpr (std::is_constant_evaluated())已经限定了常量语境了,为何还不能调用立即函数?但是constexpr和consteval的底层机理并不一致,所以并不相通。

因此为解决这个问题,新增 if consteval { } 来代替 if(std::is_constant_evaluated())。

语法:if consteval {} consteval前后不能有圆括号,后面必须接花括号

新版例子:

#include <type_traits>// 立即函数
consteval int f(int i) 
{ 
    return i; 
}
​
constexpr int g(int i) 
{
    // if consteval后面的花括号不可省略
    if consteval // 否定形式:not consteval 或 ! consteval
    {
        return f(i) + 1;
    } 
    else 
    {
        return 42;
    }
}

1.2 auto

1.2.1 新增支持数组指针的引用类型

编译器版本:GCC 12

文档链接CWG 2397: auto specifier for pointers and references to arrays

例子:

int main()
{
    int a[3];
    auto (*p)[3] = &a;
    return 0;
}

1.2.2 代替decay-copy语义

编译器版本:GCC 12

文档链接P0849R8: auto(x): decay-copy in the language

不太严谨地说,decay-copy语义其实是将一个变量复制一份生成其对应的prvalue。新特性下,auto将可以表示decay-type,auto(x)代替decay-copy语义。

例子:

#include <iostream>struct A 
{
    A() {}
​
    // 拷贝构造函数
    A(const A&) {}
    // 移动构造函数
    A(A&&) {}
};
​
void f(A&) 
{
    std::cout << "A(A&)" << std::endl;
}
void f(A&&)
{
    std::cout << "A(A&&)" << std::endl;
}
​
void h() 
{
    A a;
    std::cout << "===11====" << std::endl;
    // 输出A(A&),因为a是左值
    f(a);  
    std::cout << "===22====" << std::endl;
    // 输出A(A&&),因为a是左值,所以先调用A的拷贝构造函数创建prvalue
    f(A(a)); 
    std::cout << "===33====" << std::endl;
    // 输出A(A&&),因为a是左值,所以先调用A的拷贝构造函数创建prvalue,此处auto表示A类型
    f(auto(a)); 
    std::cout << "===44====" << std::endl;
    // 输出A(A&&),因为a是左值,所以先调用A的拷贝构造函数创建prvalue
    // 里层的auto表示A类型,返回一个prvalue;而因为里层已经prvalue,外层的auto则是什么都不干
    f(auto(auto(a))); // 
    std::cout << "===55====" << std::endl;
    // 等号左边的auto表示A*类型,等号右边的auto表示A类型
    auto t = new auto(a);
    // 此处的auto表示A类型
    f(auto(*t)); // 编译成功
    f(auto(t)); // 编译错误,因为auto表示A类型,但是t是A*类型
    delete t;
}
​

1.3 volatile

编译器版本:GCC 13

文档链接P2327R1: De-deprecating volatile compound operations

还原C++20中volatile弃用的特性

1.4 constexpr

编译器版本:GCC 13

文档链接P2448R2: Relaxing some constexpr restrictions

新增版本:C++11

放宽constexpr限制,constexpr函数内可以使用控制流语句和变量初始化操作以及运行时的条件(如if、switch、for、while等),以便更好适应实际应用场景。

例子:

#include <iostream>
​
constexpr void func(int i) 
{
    int a = i * i + i;
    if(i == 1)
    {
        std::cout << "111111" << std::endl;
    }
    else 
    {
        std::cout << "222222" << std::endl;
    }
​
    switch(a)
    {
    case 0:
        std::cout << "a = 0" << std::endl;
        break;
    case 1:
        std::cout << "a = 1" << std::endl;
        break;
    }
}
​
int main()
{
    func(10);
    return 0;
}

1.5 char8_t

编译器版本:GCC 13

文档链接P2513R3: char8_t Compatibility and Portability Fix

新增版本:C++20

主要问题:char8_t类型在不同的平台和编译器之间可能有不同的实现方式和语义,在跨平台开发时,可能会有兼容性和可移植性问题。因此C++23即修复该问题。

如下例:

extern const char* a = u8"a"; // Works in C (using default extensions), broken in C++20
extern const char b[] = u8"b"; // Works in C, broken in C++20
extern const unsigned char* c = u8"c"; // Works in C (using default extensions), broken in C++20
extern const unsigned char d[] = u8"d"; // Works in C, broken in C++20

修改的结果:

①char8_t是基本的字符类型

②确定char8_t和char类型之间的关系(char8_t是utf8字符数据,char是本地字符数据)

③将char8_t类型的标准库扩展到所有操作系统和编译器上

例子:

extern const char* a = u8"a"; // 依然编译不通过
extern const char b[] = u8"b"; // C++23可运行
extern const unsigned char* c = u8"c"; // 依然编译不通过
extern const unsigned char d[] = u8"d"; // C++23可运行

char8_t与char是不同的两种类型,其对应的指针类型也就不能直接赋值,类似于int和char

1.6 wchar_t

编译器版本:GCC 已实现

文档链接P2460R2: Relax requirements on wchar_t to match existing practices

放宽wchar_t要求,使其更加灵活,以匹配已有的实践经验。原来的标准是所有宽编码的所有字符使用一个wchar_t存储,但是实际上在windows(MSVC)一个wchar_t表示一个UTF-16字符。

结果:将已有的实践经验标准化。

2 语义语法

2.1 size_t字面量

编译器版本:GCC 11

文档链接P0330R8: Literal Suffix for (signed) size_t

新增zu作为std::size_t的字面量后缀

例子:

#include <iostream>int main()
{
    auto a = 10zu;
    auto b = 1u;
    std::cout << std::boolalpha;
    std::cout << std::is_same<decltype(a), decltype(b)>::value << std::endl;  // 输出false
    std::cout << std::is_same<decltype(a), long long>::value << std::endl;  // 输出false
    std::cout << std::is_same<decltype(a), unsigned long long>::value << std::endl; // 输出false
    std::cout << std::is_same<decltype(a), std::size_t>::value << std::endl;  // 输出true
    return 0;
}

2.2 lambda表达式的空圆括号

编译器版本:GCC 11

文档链接P1102R2: Make () more optional for lambdas

lambda表达式中空的圆括号可不写,扩展适用范围(原本是不支持的),包括:

①模板参数、②constexpr、③mutable、④consteval、⑤异常规范和noexcept、⑥属性列表、⑦返回类型、⑧requires

代码例子:

int main()
{
    // noexcept前面的圆括号
    auto a = [] noexcept {}; // C++23以前正确写法:[] () noexcept {}
    // constexpr前面的圆括号
    auto b = [] constexpr {}; // C++23以前正确写法:[] () constexpr {}
    // 返回类型
    auto c = [] -> void {}; // C++23以前正确写法:[] () -> void {}
    return 0;
}

2.3 标识符支持Unicode标准附录31

编译器版本:GCC 12

文档链接P1949R7: C++ Identifier Syntax using Unicode Standard Annex 31

2.4 允许属性重复

编译器版本:GCC 11

文档链接P2156R1: Allow Duplicate Attributes

2.5 向下类型转换为bool类型

编译器版本:GCC 9

文档链接P1401R5: Narrowing contextual conversions to bool

主要是新增支持static_assert和if constexpr表达式中。

2.6 规范行尾反斜杆

编译器版本:GCC 已支持

文档链接P2223R2: Trimming whitespaces before line splicing

规范反斜杆换行,解决多种编译器不一致的问题。

如例子:

#include <iostream>int main()
{
    int i = 1 
    // \
    + 42
    ; // 按照语义,注释行末尾加反斜杆,下一行应当也是注释行
    std::cout << i << std::endl; // MSVC输出43,GCC和Clang输出1
    return 0;
}

此处规范后,输出i为1

2.7 取消混合的字符串字面量连接语法

编译器版本:GCC 已支持

文档链接P2201R1: Mixed string literal concatenation

在标准层面不再支持混合字符串字面量的连接语法

例子:

int main()
{
    auto a = L"" u""; // 不再支持
    auto a = L"" u8""; // 不再支持
    return 0;
}

2.8 非静态数据成员的地址

编译器版本:GCC 已支持

文档链接P1847R4: Make declaration order layout mandated

明确规定,类的非静态、大小非0的数据成员,声明顺序越往后,其偏移地址越大。最初可追溯到C++03标准不够明确的表述,但其实各大编译器都非常默契地实现了。

不再举例。

2.9 移除垃圾回收的支持

编译器版本:GCC 12

文档链接P2186R2: Removing Garbage Collection Support

于C++11添加最小化的支持,可自行回顾,文档链接N2670: Minimal Support for Garbage Collection and Reachability-Based Leak Detection (revised)

关于垃圾回收的工具,可以自行搜索Boehm GC(可能某些混合使用C++和C#的unity游戏引擎的游戏也依赖于这个库),或者参考其他的一些用C++实现的支持垃圾回收的虚拟机。

移除“安全派生指针”的概念,具体概念可追溯C++11的新特性。

2.10 简化隐式移动语义

编译器版本:GCC 13

文档链接P2266R3: Simpler implicit move

一个表达式是xvalue(消亡值)的条件(之一):

①符合移动条件的变量

②无论是显示和隐式,返回类型是右值引用类型的函数的返回值

③被转换为右值引用

④以xvalue数组为操作数的下标操作

⑤访问一个xvalue对象的非引用类型的非静态数据成员

⑥.*成员指针表达式中,第一个操作数是xvalue,第二个操作数是数据成员

一般来说,有名字的右值引用视为lvalue(左值),没有名字的右值引用视为xvalue(消亡值),函数的右值引用无论是否有名字都视为lvalue(左值)

例子:

#include <utility>template<typename T>
T& f(T&& t) // t是一个转发引用(universal reference)
{
   return t; // 这里返回对t的左值引用
}
​
struct S { };
​
void g()
{
   S s1{ };
   S& s2 = f(std::move(s1)); // 将s1转为右值后调用f
}

上面例子在C++23后编译不再通过

2.11 this推导

编译器版本:GCC 14

文档链接P0847R7: Deducing this

成员函数第一个参数新增支持this,且支持模板推导

还有附属优化:按值传递的参数可优化为移动语义

struct X 
{
    // 传统方式:需要四个重载
    T& value() &;
    const T& value() const &;
    T&& value() &&;
    const T&& value() const &&;
};

在原有C++中,一个成员函数需要处理多种情况(如上面例子),这几个几乎相同的重载函数基本都是重复代码,因此引入该新特性,通过引入显式对象参数语法,实现允许成员函数显式声明其对象参数,并通过模板推导自动推断对象类型和值类别。

最终目的:

1.减少代码重复

2.在安全的情况下自动选择移动而非拷贝,减少对完美转发的过度使用

例子1-基础例子:

#include <utility>struct X 
{
    void foo(this X const& self, int i) {}
​
    template <typename Self>
    void bar(this Self&& self) {}
};
​
struct D : X { };
​
void ex(X& x, D const& d) 
{
    x.foo(42); // self绑定到x, i参数值42
    x.bar();  // 函数参数Self类型推导为X&, 实际调用形式是X::bar<X&>
    std::move(x).bar();  // 函数参数Self类型推导为X, 实际调用形式是X::bar<X>
​
    d.foo(17);  // self绑定到d
    d.bar(); // 函数参数Self类型推导为D const&, 实际调用形式是X::bar<D const&>
}

例子2-减少代码重复:

#include <utility>template<typename T>
struct X 
{
    // 传统方式:需要四个重载
    T& value() &;
    const T& value() const &;
    T&& value() &&;
    const T&& value() const &&;
​
    // 新提案方式:一个函数模板搞定
    template <typename Self>
    auto&& value(this Self&& self) {
        return std::forward<Self>(self).m_value;
    }
private:
    T m_value;
};

例子3-lambda递归:

#include <iostream>int main()
{
    auto fib = [](this auto self, int n) -> int {
        if (n < 2) return n;
        return self(n-1) + self(n-2);
    };
    std::cout << fib(10) << std::endl;
    return 0;
}

例子4-移动优化:

std::string str = "data";
​
// 情况A:str之后不再使用 → 可以移动
process(str);
// str在此之后不再被访问// 情况B:str之后还要使用 → 不能移动
process(str);
std::cout << str;  // 还需要使用,必须拷贝

例子5-完美转发优化

// 完美转发方案
template<typename T>
void wrapper_forward(T&& arg) {
    target(std::forward<T>(arg));  // 保持值类别
}
​
// P0847优化方案(更简单)
template<typename T>
void wrapper_simple(T arg) {  // 直接按值!
    target(std::move(arg));   // 总是可以移动
}
​
// 使用
std::string str = "hello";
wrapper_simple(str);  // 可能移动而非拷贝

例子6-CRTP设计模式:

//C++23前:CRTP设计模式
template <typename Derived>
struct add_postfix_increment1
{
    Derived operator++(int) 
    {
        auto& self = static_cast<Derived&>(*this);
​
        Derived tmp(self);
        ++self;
        return tmp;
    }
};
​
struct SomeType1 : add_postfix_increment1<SomeType1> 
{
    SomeType1& operator++() { /*......*/ }
};
​
//新写法:CRTP设计模式
struct add_postfix_increment2
{
    template <typename Self>
    auto operator++(this Self&& self, int) 
    {
        auto tmp = self;
        ++self;
        return tmp;
    }
};
​
struct SomeType2 : add_postfix_increment2 
{
    SomeType2& operator++() { /*......*/ }
};

2.12 更改lambda中的作用域

编译器版本:未支持

文档链接P2036R3: Change scope of lambda trailing-return-type

为消除语义冲突,更改为只有lambda主体部分能够访问捕获列表的参数

核心问题如下例子:

double x;
auto a = [x=1](decltype((x)) y){ return x; };

这个例子可产生4种语义:

①lambda中的x是double&类型。这将导致a(100)报错。

②lambda中的x是int&类型。这将导致a(100.012)报错

③lambda中的x是int const&类型。

④语法错误。

实际是何种语义,全凭编译器决定。

因此,新特性将统一规范lambda。

因编译器未支持,代码例子待续。

2.13 多维下标运算符

编译器版本:GCC 12

文档链接P2128R6: Multidimensional subscript operator

下标运算符支持变长参数,以支持多维访问

用法例子:

#include <iostream>
#include <vector>struct X
{
    std::vector<int> v;
​
    // 重载下标运算符,以实现数组元素求和
    template<typename _Index>
    int operator[](_Index &&index)
    {
        return v[index];
    }
​
    template<typename _T1, typename ... _T2>
    int operator[](_T1 &&index, _T2 &&...indexs)
    {
        return v[index] + (*this)[std::forward<_T2>(indexs)...];
    }
};
​
int main()
{
    X x;
    x.v = {1, 2, 3, 4, 5, 6, 7};
    int sum = x[1, 2, 3, 6];
    std::cout << sum << std::endl; // 输出16,因为2+3+4+7=16
    return 0;
}

2.14 constexpr函数中常量语境下的变量

编译器版本:GCC 12

文档链接P2242R3: Non-literal variables (and labels and gotos) in constexpr functions

增强编译优化,允许在constexpr函数中的常量环境下定义变量,只要不影响返回值,变量相关的代码将被优化。

例子:

#include <iostream>
​
template<typename T> 
constexpr bool f() 
{
    // 常量环境下
    if (std::is_constant_evaluated()) 
    {
        // ...
        return true;
    } 
    else 
    {
        T t; // 定义非常量对象,这将被忽略
        t(); // 依然编译通过
        // ...
        return true;
    }
}
struct nonliteral 
{ 
    nonliteral() 
    {
        std::cout << "=======" << std::endl;
    } 
​
    void operator()()
    {
        std::cout << "===operator()====" << std::endl;
    }
};
​
// 常量语境下的调用,只要f()函数内部的变量不影响返回值,则编译通过
static_assert(f<nonliteral>()); 

2.15 字符集与字符编码

编译器版本:GCC 10

文档链接P2314R4: Character sets and encodings

新特性将支持以下上下文中使用unicode编码:

⑴asm内联汇编声明语句的编译环境

⑵#include文件名

⑶语言关联

⑷operator ""

⑸#line指令

⑹nodiscard和deprecated属性的提示文本

⑺#error和static_assert的提示文本

FILEfunc的字符串名称

⑼std::typeinfo::name()

⑽字符字面量或字符串字面量

⑾用户自定义的字面量

2.16 一致的字符字面量编码

编译器版本:已支持

文档链接P2316R2: Consistent character literal encoding

无需支持字符集,即可支持完全可移植的编码,并可以在不同的机器上通过编译器的编码转换而保持相同的编码。当前支持unicode编码转义文字。

2.17 初始化声明新增支持别名声明

编译器版本:GCC 12

文档连接:P2360R0: Extend init-statement to allow alias-declaration

选择结构、基于范围的循环结构 的初始化语句新增支持别名声明

例子:

#include <iostream>
​
​
int main()
{
    int a[10] = {0};
    int b = 100;
    if(typedef int T; b < 100)
    {
        /* ... */
    }
    for(using T = int; T v : a) 
    {
        std::cout << v << std::endl;
    }
    return 0;
}

2.18 lambda表达式新增属性支持

编译器版本:GCC 9

文档链接P2173R1: Attributes on lambda-expressions

例子:

auto lm = [] [[nodiscard]] ()->int { return 42; };

2.19 复合语句末尾的标签

编译器版本:GCC 13

文档链接P2324R1: Labels at the end of compound statements(C compatibility)

C++与C的标签还存在不兼容的地方,标签可以附加到所有的语句,但是C++不能将标签放到复合语句的末尾。

例子:

void foo(void)
{
first: // 这里的标签C++和C都支持
    int x;
second: // 这里的标签C++和C都支持
    x = 1;
last: // 这里最末尾的标签C支持,但C++不支持
}
​
int main()
{
    int a = 10;
    // 这里的标签C++支持,但C不支持,C需要用花括号
    if(a > 10)
test_label1: int x; 
​
    // 新版写法:
    if(a > 10)
    {
test_label2: 
        int x; 
    }
    return 0;
}

2.20 重写==和!=运算符

编译器版本:GCC 13

文档链接P2468R2: The Equality Operator You Are Looking For

重载operator==(A, B)时

①如果未重载operator!=(A, B),则对operator==(B, A)、operator!=(A, B)、operator!=(B, A)生效。

②如果已重载operator!=(A, B),则对operator==(B, A)、operator!=(A, B)、operator!=(B, A)不生效。

③如果已重载operator==(B, A)、operator!=(B, A),则优先调用operator==(B, A)、operator!=(B, A)。

但对于operator!=(A, B)没有这样的效果

代码如下:

#include <type_traits>
#include <iostream>struct A {};
​
template<typename T> 
bool operator==(A, T)   // #1
{ 
    std::cout << "bool operator==(A, T)" << std::endl;
    return true; 
}
​
bool a1 = 0 == A();  // ok,调用#1
bool a2 = 0 != A();  // ok,调用#1
bool a3 = A() == 0;  // ok,调用#1
bool a4 = A() != 0;  // ok,调用#1
​
template<typename T> 
bool operator!=(A, T)  // #2
{ 
    std::cout << "bool operator!=(A, T)" << std::endl;
    return true;
}
​
bool a5 = 0 == A();  // 编译错误,没有找到operator==(int, A)
bool a6 = 0 != A();  // 编译错误,没有找到operator!=(int, A)
bool a7 = A() == 0;  // ok,调用#1
bool a8 = A() != 0;  // ok,调用#2struct B 
{
    bool operator==(const B&)   // #3
    { 
        std::cout << "bool B::operator==(const B&)" << std::endl;
        return true; 
    }
};
​
struct C : B 
{
    C() {}
    C(B) {}
    bool operator!=(const B&)   // #4
    { 
        std::cout << "bool C::operator!=(const B&)" << std::endl;
        return true; 
    } 
};
​
bool c1 = B() == C();  // ok,调用#3
bool c2 = C() == B();  // C对象的operator==(const B&) 和 B对象的operator==(const B&)冲突
                       // GCC 13编译不会报错,但有警告struct D {};
​
template <typename T>
bool operator==(D, T) // #5
{ 
    std::cout << "bool operator==(D, T)" << std::endl;
    return true; 
}
​
inline namespace N 
{
    template <typename T>
    bool operator!=(D, T) // #6
    { return true; }
}
​
bool d1 = 0 == D(); // 编译错误,当前命名空间下,operator==(D, int)和operator!=(D, T)都已重载
                    // 找不到operator==(int, D)的重载实现
​
​

2.21 移除无法编码的宽字符字面量和多字符宽字符字面量

编译器版本:GCC 13

文档链接P2362R3: Remove non-encodable wide character literals and multicharacter wide character literals

这篇文档不是不再支持宽字符字面量语法,而是建议如何移除无法编码的情况,使代码更加健壮和易读。

例子1:

#include <iostream>int main()
{
    std::cout << 'ab' << std::endl;// a是0x61,b是0x62,所以输出24930(即0x6162)
    // 因此,建议拆分成'a'和'b'
    return 0;
}

例子2:

#include <iostream>
​
​
int main()
{
    // 类似这种unicode字符集的宽字符,可以使用L前缀来表示宽字符
    std::wcout << L'\u0001F525' << std::endl;
    return 0;
}

2.22 新的八进制、十六进制和universal-character-name的转义序列

编译器版本:GCC 13

文档链接P2290R3: Delimited escape sequences

universal-character-name转义序列:即使用 4或8个十六进制数字16或32位 来表示的unicode标量值

新的转义字符表示方法:

const char *ch1 = "\u{0001F1F8}"; // unicode
const char *ch2 = "\o{053724}"; // 八进制
const char *ch3 = "\x{0001F1F8}"; // 十六进制

2.23 新增unicode编码的字符序列的转义序列

编译器版本:GCC 13

文档链接P2071R2: Named universal character escapes

新增 \N{名称} 语法来标识标准unicode字符序列,暂时只适用于字符和字符串。

例子:

const char *ch = "\N{0001F1F8}\N{U+000100}";

2.24 常量表达式中使用未知的指针和引用

编译器版本:GCC 14

文档链接P2280R4: Using unknown pointers and references in constant expressions

本质的改动,就是把 可以在编译期计算出结果的运行期变量 在编译期计算出来而且不用写constexpr等修饰词。

例子:

int func1(int a)
{
    return a + a;
}
​
int func2(int *a)
{
    return (*a) * (*a);
}
​
int main()
{
    int a = 10;
    int b = 2;
    int c = func1(b); // 此处因为b可在编译期计算得到2,所以c的结果可直接在编译期得到4
    int d = func2(&a); // 此处因为b可在编译期计算得到10,所以c的结果可直接在编译期得到100
    return 0;
}

2.25 static operator()

编译器版本:GCC 13

文档链接P1169R4: static operator()

目前的括号运算符重载函数都是以非静态成员函数的方式实现,而对于STL中的接口,需要传入带有括号运算符函数的类型(例如std::less等),如果该类型的括号重载函数没有内联,那么在使用时还得必须创建对应类型的对象,也就需要使用一个额外的寄存器存入对象的this指针。

例子:

#include <vector>
#include <algorithm>struct A
{
    static bool operator()(int a, int b)
    { return a > b; }
};
​
int main()
{
    std::vector<int> v;
    std::sort(v.begin(), v.end(), std::greater<int>());
    
    using CmpType = decltype(&A::operator()); // CmpType被推导为std::function<bool(int, int)>
    std::sort(v.begin(), v.end(), CmpType()); // C++23支持的形式
    return 0;
}

汇编的结果可自行查看,静态的operator()相比于非静态的operator(),少了一层偏移量的设定。

2.26 新增浮点数类型别名

编译器版本:GCC 13

文档链接P1467R9: Extended floating-point types and standard names

新增类型:std::float16_t、std::float32_t、std::float64_t、std::float128_t

分别代表16位、32位、64位、128位的浮点数,不再赘述

2.27 从继承的构造函数中推导模板参数

编译器版本:GCC 14

文档链接P2582R1: Wording for class template argument deduction from inherited constructors

在C++17中就可以利用构造函数来推导模板参数(请自行回顾),在C++23中,这种办法扩展到了继承机制,可以从基类继承过来的构造函数中推导模板参数

例子:

template<typename T> 
struct B 
{
    B(T) {}
};
​
template<typename T> 
struct C : public B<T> 
{
    using B<T>::B;
};
​
template<typename T> 
struct D : public B<T> 
{ };
​
C c(42); // 编译通过,c类型推导为C<int>
D d(42); // 编译不通过,没有引入可推导的构造函数
B(int) -> B<char>; // 显式声明推导类型
C c2(42); // 编译通过,c2对象被推导为C<char>template<typename T> 
struct E : public B<int> 
{
    using B<int>::B;
};
​
/**
 * 编译不通过,因为上面B(int)已经显式声明推导类型是B<char>了
 * E中using的是B<int>类型的构造函数
 * 没有引入可推导的路径
 */ 
E e(42);
​
E<int> e2(42); // 编译通过template<typename T, typename U, typename V> 
struct F 
{
    F(T, U, V) {}
};
​
template<typename T, typename U> 
struct G : F<U, T, int> 
{
    using G::F::F;
};
​
G g(true, 'a', 1); // 编译通过,g对象推导为G<char, bool>

2.28 支持UTF8作为可移植的源文件编码

编译器版本:GCC 13

文档链接P2295R6: Support for UTF-8 as a portable source file encoding

2.29 显式生命周期管理

编译器版本:编译器未实现

文档链接P2590R2: Explicit lifetime management

个人理解,思想上类似一个内存池(并非对象池的那种内存池),当需要创建对象(无论什么类型)时,都可以直接从同一个内存池中获得内存地址。

因编译器没实现,就不多说了。

2.30 static operator[]

编译器版本:GCC 13

文档链接P2589R1: static operator[]

operator[]增加支持static修饰,略

2.31 允许constexpr函数中定义static constexpr变量

编译器版本:GCC 13

文档链接P2647R1: Permitting static constexpr variables in constexpr functions

比较简单,直接看例子:

constexpr int func(int a, int b)
{
    static constexpr int v = 100; // C++23起支持static constexpr
    return a + b + v;
}

2.32 consteval向上传导

编译器版本:GCC 14

文档链接P2564R3: consteval needs to propagate up

consteval表达式新增支持向上传导

C++23新增支持的例子:

consteval int id(int i) { return i; }
constexpr char id(char c) { return c; }
​
template <typename T>
constexpr int f(T t) {
    return t + id(t);
}
​
auto a = &f<char>; // 编译通过,f<char> 是立即函数
auto b = &f<int>;  // 编译错误:f<int>不是立即函数static_assert(f(3) == 6); // 编译期断言,编译通过template <typename T>
constexpr int g(T t) {    // g<int>不是立即函数
    return t + id(42);    // 因为id(42)是一个常量
}
​
template <typename T, typename F>
constexpr bool is_not(T t, F f) {
    return not f(t);
}
​
consteval bool is_even(int i) { return i % 2 == 0; }
​
static_assert(is_not(5, is_even)); // 编译期断言,编译通过int x = 0;
​
template <typename T>
constexpr T h(T t = id(x)) { // h<int>不是立即函数
    return t;
}
​
template <typename T>
constexpr T hh() {           // hh<int>是立即函数
    return h<T>();
}
​
int i = hh<int>(); // 编译错误: hh<int>()表达式处于立即语义向上传导的函数的外面struct A {
  int x;
  int y = id(x);
};
​
template <typename T>
constexpr int k(int) {  // k<int>不是立即函数
  return A(42).y;       // 因为A(42)本身就是常量表达式,所以不会向上传导
}                       

2.33 更有意义的export

编译器版本:GCC 15

文档链接P2615R1: Meaningful exports

规范哪些声明可以被export,防止语义模糊或无效

例子:

export module M;
​
// 错误示例:直接导出无意义的声明
export namespace {}       // 错误:空的命名空间不引入任何名字
export using namespace N; // 错误:直接使用using不声明名字namespace {
    export int a2;        // 错误:尝试导出具有内部链接(在匿名命名空间内)的名字
}
​
export static int b;      // 错误:显式用static声明了内部链接// 正确示例:导出有意义的声明
export int f();           // 正确:导出一个函数
export namespace N { }    // 正确:导出一个(非空的)命名空间定义
                          // 虽然这里的N是空的,但语法上它声明了命名空间N这个名字

2.34 修复基于范围的for循环的问题

编译器版本:GCC 15

文档链接P2718R0: Wording for P2644R1 Fix for Range-based for Loop

修复for循环中临时对象的生命周期问题

#include <list>using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t)        { return t; }
​
T g()
{ return T(); }
​
void foo() {
    for (auto e : f1(g())) {}  // 正确,将g()返回值的生命周期扩展至整个for循环
    for (auto e : f2(g())) {}  // 未定义行为,运行时将会触发程序中断
}

3 预处理指令

3.1 elifdef和elifndef

编译器版本:GCC 12

文档链接P2334R1: Add support for preprocessing directives elifdef and elifndef

新增预处理命令,用于ifdef和ifndef的else部分的条件。

例子:

#ifdef MY_IF1
#elifdef MY_IF2
#endif#ifndef MY_IF1
#elifndef MY_IF2
#endif#ifdef MY_IF1
#elifndef MY_IF2
#endif#ifndef MY_IF1
#elifdef MY_IF2
#endif

3.2 warning

编译器版本:GCC 13

文档链接P2437R1: Support for #warning

用于在预处理阶段放出警告

语法格式:#warning [text]

例子:

#warning "ahhahaha"

4 属性

4.1 assume

编译器版本:GCC 13

文档链接P1774R8: Portable assumptions

新增编译器指令,用于提示编译器某一个假设条件一定成立,可以忽略某些可能发生的错误检查,以便编译器更好地优化代码

语法格式:[[assume(expr)]]

其中expr为可得到bool的表达式

用法例子:

[[assume(expr1, expr2)]]; // Error
[[assume((expr, expr2))]]; // OK
[[assume(x = 1)]]; // Error
[[assume(x == 1)]]; // OK
[[assume((x = 1))]]; // OK

优化例子:

int f1(int x) 
{
    [[assume(x >= 0)]]; // 假设x是不小于0的
    return x / 32; // 可能会省略负值的处理
}
​
int f11(int x) 
{
    return x / 32; // 可能会省略负值的处理
}
​
int f2(int y) 
{
    [[assume(++y == 43)]]; // 假设y+1等于43
    return y; // 该return语句可能被替换为return 42;
}
​
int f22(int y) 
{
    return y; // 该return语句可能被替换为return 42;
}

对应的汇编代码(开启-O2编译选项,可以看得到优化效果)

f1(int):
        mov     eax, edi
        sar     eax, 5
        ret
f11(int):
        test    edi, edi
        lea     eax, [rdi+31]
        cmovns  eax, edi
        sar     eax, 5
        ret
f2(int):
        mov     eax, 42
        ret
f22(int):
        mov     eax, edi
        ret

5 标准库

推荐网站:www.apiref.com/cpp-zh/cpp/…

不再列举

6 标准文档链接

open-std.org/JTC1/SC22/W…