这篇博客为自己学习现代C++特性的笔记,参考书为《modern-cpp-tutorial-zh-cn》。
语言可用性的强化
常量
nullptr
目的:取代NULL。
原因:编译器会将 NULL 定义为 0 或者 (void*) 0 。
nullptr 的类型为 nullptr_t ,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
constexpr
目的:让用户显式地声明函数或对象构造函数在编译期会成为常量表达式。
(C++ 标准中数组的长度必须是一个常量表达式)
1 | int len = 10; |
此外,constexpr修饰的函数可以使用递归:
1 | constexpr int fibonacci(const int n){ |
变量及其初始化
if/switch 变量声明强化
可以在 if 里声明临时变量:
1 | if(const std::vector<int>::iterator itr = std::find(vec.begin(), vec.end(), 3); |
初始化列表
给类对象的初始化与普通数组和POD (Plain Old Data,即没有构造、析构和虚函数的类或结构体)类提供统一使用{}进行初始化。
1 |
|
结构化绑定
有一个临时要用的 tuple, 可以使用结构化绑定简单拿到里面的元素。
1 | std::tuple<int, double, std::string> f(){ |
类型推导
auto
用于替换冗长的迭代写法,让编译器进行类型推导。
1 | auto i = 5; |
注:auto不能用于推导数组类型。
decltype
目的:推导表达式的类型。
1 | auto x = 1; |
尾返回类型推导
auto 用于函数返回类型推导。
1 | template<typename T, typename U> |
decltype(auto)
decltype(auto) 主要用于对转发函数或封装的返回类型进行推导,它使我们无需显式的指定 decltype 的参数表达式。
1 | std::string lookup1(); |
控制流
if constexpr
可以让代码在编译时期就完成分支判断,提高效率。
1 | template<typename T> |
区间for循环
1 | for(auto element : vec){ |
模板
模板的哲学在于将一切能够在编译期处理的问题丢到编译期进行处理,仅在运行时处理那些最核心的动态服务,进而大幅优化运行期的性能。
外部模板
目的:可以显式地通知编译器何时进行模板的实例化。
原因:在传统c++种,模板只有使用时才会被编译器实例化。如果每个文件中编译的代码都遇到了被完整定义的模板,就会重复实例化,导致编译时间的增加。
1 | template class std::vector<bool> // 强行实例化 |
尖括号 “>”
1 | std::vector<std::vector<int>> matrix; |
“>>” 传统一律当成右移, c++11以后合法。
类型别名模板
代替 typedef 原名称 新名称 对函数指针等别名的定义语法。
1 | using NewProcess = int(*)(void *); |
变长参数模板
目的:允许任意个数、任意类别的模板参数,同时也不需要在定义时将参数的个数固定。
1 | template<typename... Ts> class Magic; //参数个数可以为0 |
折叠表达式
1 |
|
面向对象
委托构造
构造函数可以在同一个类中一个构造函数调用另一个构造函数,达到代码简化的目的。
1 | class Base{ |
继承构造
1 | class Subclass : public Base(){ |
显式虚函数重载
override
显式地告知编译器进行重载,如果编译器没有检查到这样的虚函数,将无法通过编译。
1 | struct Base { |
final
目的:为了防止类被继续继承以及终止虚函数继续重载。
1 | struct Base { |
显式禁用默认函数
1 | class Magic { |
强类型枚举
1 | enum class new_enum : unsigned int { |
实现了枚举的类型安全。
语言运行期的强化
Lambda表达式
基础
基本语法:
1 | [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 { |
捕获列表:传递外部数据给内部函数。
值捕获:前提是变量可以拷贝,被捕获的变量在Lambda表达式被创建时拷贝,而非调用时拷贝。
引用捕获:保存的是引用,值会发生变化。
隐式捕获:写一个 & 或者 = 向编译器声明采用引用捕获或者值捕获。
捕获列表最常用的四种形式:
- [] 空捕获列表
- [name1, name2, …] 捕获一系列变量
- [&] 引用捕获
- [=] 值捕获
表达式捕获:(需要了解右值引用以及智能指针)
泛型Lambda
在形参声明中使用auto类型指示说明符的lambda。
1 | auto add = [](auto x, auto y){ |
函数对象包装器
std::function
是一种通用、多态的函数封装,是类型安全的。是函数的容器。
1 |
|
std::bind 和 std::placeholder
1 | int foo(int a, int b, int c){ |
右值引用
左值、右值的纯右值、将亡值、右值
左值:表达式左边的值,表达式后依然存在的持久对象。
右值:表达式右边的值,表达式结束后不再存在的临时对象。
纯右值:要么是纯粹的字面量,比如10, true,要么是求值结果相当于字面量或匿名临时对象,比如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、原始字面量,Lambda表达式都属于纯右值。
注意:字符串字面量只有在类中才是右值,当其位于普通函数中是左值。
将亡值:即将被销毁、却能被移动的值。
1 | std::vector<int> foo(){ |
v把foo()返回的temp复制一份,然后把temp销毁。但是,v 可以被别的变量捕获到,而foo() 产生的那个返回值作为一个临时值,一旦被v 复制后,将立即被销毁,无法获取、也不能修改。而将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动。
右值引用和左值引用
要拿到一个将亡值,需要用到右值引用:T &&,其中T是类型。右值引用的声明让这个临时值的声明周期得以延长。
std::move 将左值参数无条件地转换为右值。
右值引用本身是一个左值。
1 | std::string lv1 = "string,"; // lv1 是一个左值 |
移动语义
目的:实现对对象的移动操作,而不是先复制、再析构,从而提高性能。
1 | int main(){ |
完美转发
std::forward 来进行参数的转发(传递)。
目的:在传递参数的时候,保持原来的参数类型(左引用保持左引用,右引用保持右引用)。
例如右值引用其实是一个左值,但传递的时候也需要保持右值引用,所以需要完美转发。
1 |
|
容器
线性容器
std::array
- 为什么不用vector?
- 为什么不用传统数组?
对于1,array的大小是固定的,vector会自动扩容,扩容以后内存需要手动释放。
对于2,array让代码更“现代化”,可以使用一些封装好的函数以及stl的容器算法。
std::forward_list
是一个列表容器,和std::list类似。使用单向链表实现,插入复杂度是O(1),不支持快速随机访问。当不需要双向迭代时,具有比std::list 更高的空间利用率。
无序容器
有序容器:std::map/std::set 内部通过红黑树实现,插入和搜索平均复杂度为O(logn)。
无序容器:std::unordered_map/std::unordered_multimap和 std::unordered_set/std::unordered_multiset内部通过哈希表实现,内部元素是无序的。
元组
目的:存放不同类型的数据。
元组基本操作
- std::make_tuple 构造元组
- std::get 获得元组某个位置的值
- std::tie 元组拆包
1 |
|
运行期索引
std::variant<> 可以让一个variant<> 容纳提供的几种类型的变量。variant 可以存放多种类型的数据,但任何时刻最多只能存放其中一种类型的数据。variant 无须借助外力只需要通过查询自身就可辨别实际所存放数据的类型。
元组合并
std::tuple_cat 可以合并两个元组。
1 | auto new_tuple = std::tuple_cat(get_student(1), std::move(t)); |
智能指针与内存管理
RAII和引用计数
RAII(Resource Acquisition Is Initialization):资源获取即初始化技术。
引用计数:为了防止内存泄漏。
std::shared_ptr
std::shared_ptr 是一种智能指针,它能够记录多少个shared_ptr 共同指向一个对象,从而消除显式的调用delete,当引用计数变为零的时候就会将对象自动删除。
使用std::make_shared 来消除显式地调用new,返回这个对象类型的std::shared_ptr指针。
1 |
|
std::shared_ptr 可以通过get() 方法来获取原始指针,通过reset() 来减少一个引用计数,并通过use_count() 来查看一个对象的引用计数。
1 | auto pointer = std::make_shared<int>(10); |
std::unique_ptr
std::unique_ptr 是一种独占的智能指针,它禁止其他智能指针与其共享同一个对象,从而保证代码的安全。
1 | std::unique_ptr<int> pointer = std::make_unique<int>(10); |
可以利用std::move将其转移给其他的unique_ptr。
1 | std::unique_ptr<Foo> p2(std::move(p1)); |
std::weak_ptr
std::weak_ptr 是一种弱引用(相比较而言std::shared_ptr 就是一种强引用)。弱引用不会引起引用计数增加。
正则表达式
简介
需求:
- 检查一个串是否包含某种形式的子串;
- 将匹配的子串替换;
- 从某个串中取出符合条件的子串。
正则表达式是由普通字符(例如a 到z)以及特殊字符组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。正则表达式作为一个模板,将某个字符模式与所搜索的字符串进行匹配。
普通字符
所有大小写字母、所有数字、所有标点符号和一些其它符号。
特殊字符
限定符
std::ragex及其相关
C++11 提供的正则表达式库操作std::string 对象,模式std::regex (本质是std::basic_regex)进行初始化,通过std::regex_match 进行匹配,从而产生std::smatch(本质是std::match_results对象)。
1 |
|
并行与并发
并行基础
std::thread 用于创建一个执行的线程实例。
1 |
|
互斥量与临界区
std::lock_guard是一个RAII语法的模板类。
1 |
|
更好的做法是std::unqiue_lock,会以独占所有权的方式管理mutex。std::lock_guard 不能显式的调用lock 和unlock,而std::unique_lock 可以在声明后的任意位置调用,可以缩小锁的作用范围,提供更高的并发度。
1 | void critical_section(int change_v) { |
期物
std::future,它提供了一个访问异步操作结果的途径,可以用来获取异步任务的结果。类似于线程同步的手段,屏障(barrier)。
1 |
|
条件变量
std::condition_variable 为了解决死锁而引入。condition_variable 实例被创建出现主要就是用于唤醒等待线程从而避免死锁。std::condition_variable 的notify_one() 用于唤醒一个线程;notify_all()则是通知所有线程。
1 |
|
原子操作与内存模型
原子操作
std::atomic能够实例化一个原子类型,将一个原子类型读写操作从一组指令,最小化到单个CPU 指令。
1 | std::atomic<int> counter; |
可以通过std::atomic
一致性模型
- 线性一致性:又称强一致性或原子一致性。它要求任何一次读操作都能读到某个数据的最近一次写
的数据,并且所有线程的操作顺序与全局时钟下的顺序是一致的。 - 顺序一致性:同样要求任何一次读操作都能读到数据最近一次写入的数据,但未要求与全局时钟的
顺序一致。 - 因果一致性:它的要求进一步降低,只需要有因果关系的操作顺序得到保障,而非因果关系的操作
顺序则不做要求。 - 最终一致性:是最弱的一致性要求,它只保障某个操作在未来的某个时间节点上会被观察到,但并
未要求被观察到的时间。
内存顺序
C++11 为原子操作定义了六种不同的内存顺序std::memory_order 的选项,表达了四种多线程间的同步模型:
- 宽松模型:在此模型下,单个线程内的原子操作都是顺序执行的,不允许指令重排,但不同线程间原子操作的顺序是任意的。类型通过std::memory_order_relaxed 指定。
- 释放/消费模型:在此模型中,我们开始限制进程间的操作顺序,如果某个线程需要修改某个值,但另一个线程会对该值的某次操作产生依赖,即后者依赖前者。
- 释放/获取模型:在此模型下,我们可以进一步加紧对不同线程间原子操作的顺序的限制,在释std::memory_order_release 和获取std::memory_order_acquire 之间规定时序,即发生在释放(release)操作之前的所有写操作,对其他线程的任何获取(acquire)操作都是可见的,亦即发生顺序(happens-before)。
- 顺序一致模型:在此模型下,原子操作满足顺序一致性,进而可能对性能产生损耗。可显式的通过std::memory_order_seq_cst 进行指定。
一些杂项
新类型
long long int
至少具备64位的比特数。
noexcept的修饰和操作
C++11 将异常的声明简化为以下两种情况:
- 函数可能抛出任何异常
- 函数不能抛出任何异常
1 | void may_throw(); // 可能抛出异常 |
使用 noexcept 修饰过的函数如果抛出异常,编译器会使用 std::terminate() 来立即终止程序运行。
字面量
原始字符串字面量
可以在一个字符串前方使用R 来修饰这个字符串,同时,将原始字符串使用括号包裹。
1 |
|