C++17 新增特性总结
本文对 C++ 17 引入的新特性做一个概览, 列举其主要用法. 限于文章篇幅, 本文不对新特性做深入探讨, 有兴趣的读者可以查找相关资料做进一步了解.
语言特性
结构化绑定(Structured Bindings)
结构化绑定允许你将一个结构体, 元组或数组的元素直接绑定到命名变量上, 下面是几个 C++17 结构化绑定的示例:
带初始化的 if
/switch
在下面的例子中, if
语句中声明了一个局部变量it
, 并且在if
语句块中使用这个变量.
inline
功能增强
C++17 增加了两个新的特性:
- 在头文件中用
inline
修饰变量, 而不用担心重复定义的问题. - 在类中声明
static inline
成员变量时, 可以直接初始化.
更多类初始化的内容: C++ 类成员初始化发展历程(从 C++11 到 C++20)
聚合扩展
在 C++17 之前, 从其他结构派生的结构会禁用聚合初始化, 开发者必须定义构造函数. C++17 引入聚合体扩展后, 聚合体可以拥有基类, 支持使用花括号进行列表初始化, 并且可以省略一些嵌套括号, 使代码更加简洁.
强制省略拷贝
在 C++17 之前, 复制省略(Copy Elision)是一种编译器优化手段, 允许编译器在某些情况下省略对象的复制或移动操作, 但这并非强制要求, 代码仍然需要保证复制构造函数和移动构造函数的可调用性.
而在 C++17 中, 在特定的初始化场景下, 复制省略成为强制规则, 即使类没有定义复制或移动构造函数, 相关的初始化操作也能正常进行.
强制复制省略的场景:
- 返回临时对象: 当函数按值返回一个临时对象(prvalue)时, 强制复制省略会发生, 直接在调用处初始化目标对象, 而不会进行复制或移动操作.
- 直接初始化: 使用临时对象直接初始化另一个对象时, 也会触发强制复制省略.
lambda 表达式增强
constexpr
lambda
lambda 捕获 *this
嵌套的命名空间
新增 attributes
[[nodiscard]]
: 用于标记函数返回值不应该被忽略. 如果忽略带有[[nodiscard]]
属性的函数返回值, 编译器通常会给出警告, 有助于避免因忽略重要返回值而可能导致的逻辑错误.[[fallthrough]]
: 用于在switch
语句中显式表明允许从一个case
标签穿透下降到下一个case
标签, 避免编译器给出警告.[[maybe_unused]]
: 用于告诉编译器某个变量, 函数参数, 类成员等可能不会被使用, 从而避免编译器给出未使用变量的警告.
utf-8 字符字面量
从 C++11 开始有 string
类型的字面量, C++17 引入了 utf-8 字符串字面量.
更多关于字面量的内容: Modern C++ 字面量一网打尽
noexcept
限定符成为类型系统的一部分
在 C++17 中, noexcept
关键字被引入了类型系统中, 意味着是否有noexcept
将影响函数类型.
f1
和f2
是不同的类型.
表达式求值顺序
C++17 中, 规定了表达式求值顺序如下:
- 如下形式的表达式中,
e1
先于e2
求值e1[e2]
e1.e2
e1.*e2
e1->*e2
e1 << e2
e1 >> e2
- 赋值语句中, 右侧的表达式先求值
e2 = e1
e2 += e1
e2 *= e1
new Type(e)
操作符中, 先分配内存, 再计算参数e
样例代码
放松枚举从整型初始化的限制
在 C++17 之前, 对于具有固定底层类型的枚举, 使用整数值进行直接列表初始化是不允许的, 而 C++17 放宽了这一限制.
改进auto
直接初始化行为
auto
类型推到的行为在 C++17 中进行了修改:
hexadecimal floating literals
单个参数的 static_assert
static_assert
允许只有一个参数.
预处理宏 __has_include
检查特定的头文件是否能被包含. 也就是检查系统是否存在该头文件.
模板编程
类模板参数推导
Class Template Argument Deduction (CTAD) 允许编译器根据传递给构造函数的参数自动推导模板参数类型, 从而简化了模板类的使用. 在此之前, 必须显式指定所有模板参数类型. CTAD 支持多种初始化方式, 并且可以用于函数模板, 类模板和通用库.
容器元素类型的推导: 例如,
std::vector v1{1, 2, 3};
自动推导为std::vector<int>
.非类型模板参数的推导: 可以从传递的初始数组中推导元素类型和大小, 例如:
变参模板的支持: 例如,
std::tuple
可以自动推导多个类型:
编译时 if
编译时 if(Compile-Time if) 允许在编译时根据条件选择执行不同的代码分支. 与运行时的 if
语句不同, if constexpr
只会在编译时评估其条件, 并且只有满足条件的分支会被实例化和编译, 不满足条件的分支则会被完全忽略. 这一特性在模板编程中特别有用, 因为它可以避免无效代码路径导致的编译错误.
类型转换和处理
根据传入参数的类型自动选择合适的处理逻辑, 例如字符串, 整数或浮点数的不同处理方式.
返回值完美转发
当需要对返回值进行处理后再返回时, 可以根据返回类型是否为 void
来决定如何处理.
标签调度
可以在一个函数内实现多态行为, 而不需要为每种类型提供重载函数.
更多阅读:
折叠表达式(Fold Expressions)
Fold Expressions (折叠表达式) 提供了一种简洁的方法来对参数包(parameter pack)中的所有元素应用二元操作符. 这项特性极大地简化了模板编程中对变长参数列表的操作, 避免了递归模板实例化带来的复杂性和性能开销.
基本概念
折叠表达式允许使用一个二元操作符将参数包中的所有元素"折叠"成单一结果.
支持两种主要形式: 左折叠(left fold)和右折叠(right fold).
- 左折叠:
(... op args)
, 例如((arg1 op arg2) op arg3) op ...
- 右折叠:
(args op ...)
, 例如arg1 op (arg2 op (arg3 op ...))
- 左折叠:
应用场景
简单的数值求和: 可以轻松实现对多个参数的加法操作.
字符串连接: 通过折叠表达式连接多个字符串或标准库类型对象.
类型检查: 用于检查一组类型是否相同(如判断是否为同质类型).
将字符串作为模板参数
C++17 允许在模板中使用字符串字面量(string literals)作为非类型模板参数. 这项特性扩展了模板编程的能力, 使得可以更灵活地处理字符串常量.
- 非类型模板参数可以是整型值(包括枚举), 指向对象/函数/成员的指针, 左值引用或
std::nullptr_t
类型. - 在 C++17 之前, 不能直接将字符串字面量传递给模板参数, 因为它们需要链接属性(linkage), 而字符串字面量没有链接属性.
- C++17 允许具有内部链接(internal linkage)的指针作为非类型模板参数, 从而可以直接在模板中使用字符串字面量.
auto
作为模板参数
C++17 允许在模板中使用auto
关键字来声明非类型的模板参数. 这项特性使得模板可以接受不同类型和值的参数, 从而增强了模板编程的灵活性和通用性.
基本概念
- 在 C++17 之前, 非类型的模板参数必须是具体类型(如整型, 指针等), 并且类型必须在模板定义时明确指定.
- 使用
auto
作为模板参数, 可以让编译器根据实际传递的参数自动推导出其类型和值, 而不需要显式指定类型. - 可以使用
auto
声明非类型模板参数, 支持多种类型, 如整数, 字符, 布尔值等, 但不支持浮点数类型.
应用场景
- 编译时常量表达式的简化: 可以通过
auto
更简洁地定义编译时常量. - 变量模板中的应用: 可以在变量模板中使用
auto
作为模板参数, 进一步增强模板的灵活性.
- 编译时常量表达式的简化: 可以通过
使用auto
作为非类型模板参数
通过使用auto
, 可以避免为每个可能的类型编写多个模板特化版本.
使用auto
定义编译时常量
扩展的 using 声明(Extended Using Declarations)
C++17 允许在类定义中使用逗号分隔的列表来导入多个基类成员. 这项特性简化了代码编写, 减少了冗余, 并提高了代码的可读性和维护性.
功能特性
- Using 声明扩展: C++17 允许在一个
using
声明中通过逗号分隔的方式导入多个基类成员. - 变长模板参数包: 可以利用变长模板参数包(variadic template parameter packs)和
using
声明来泛化地继承操作.
示例 1: 多基类成员导入
可以在派生类中通过一个using
声明导入多个基类成员, 而不需要为每个成员单独写一个using
声明.
示例 2: Lambda 重载集
通过使用using
声明, 可以从传递的基础类型中"继承"所有函数调用操作符, 从而创建一组 lambda 重载.
示例 3: 构造函数继承
可以通过using
声明继承基类的所有构造函数, 减少重复代码.
新增标准库工具
std::optional
关于std::optional
, std::variant
和std::any
的更多信息, 可以参考: 现代 C++ 必备知识: 解锁 std::optional, std::variant 和 std::any
std::variant
关于std::optional
, std::variant
和std::any
的更多信息, 可以参考: 现代 C++ 必备知识: 解锁 std::optional, std::variant 和 std::any
std::any
关于std::optional
, std::variant
和std::any
的更多信息, 可以参考: 现代 C++ 必备知识: 解锁 std::optional, std::variant 和 std::any
std::byte
string views
进一步阅读: 深入理解 C++ std::string_view — 高效字符串操作的利器
filesystem
库
关于filesystem
库的更多信息, 可以参考: C++17 文件系统库
类型系统(type traits) 扩展
C++17 新增了_v
后缀的类型特征,如:
关于 type traits 的更多内容, 请参考: [理解 C++ Type Traits]/posts/understand-cpp-type-traits/
parallel STL algorithms
给 STL 新增了执行策略.
关于 STL 算法执行策略的更多信息, 请参考: C++17 并行计算利器: 深入理解 std::execution 及其性能优化`
子串和子序列的搜索算法
C++17 引入了 Boyer-Moore and Boyer-Moore-Horspool 搜索算法.
other utilities
size()
,empty()
anddata()
: 通用版本的, 并非成员函数的std::as_const()
: 将参数转为const
类型clamp()
: 如果数值v
在范围[lower, upper]
内, 返回v
, 否则返回离它最近的值sample()
: 取样函数for_each_n()
: 迭代器版本
多线程和并发
- supplementary synchronization primitives
std::shared_mutex
/std::scoped_lock
: 参考 现代 C++锁介绍is_always_lock_free
删除的特性
std::auto_ptr
- deprecated function objects
std::random_shuffle
std::unexpected
- the obsolete
iostreams
aliases, - trigraphs
- the
register
keyword, bool
increment,- dynamic exception specification
Tags: