0%

C++ 迭代器介绍

迭代器概念

Iterator(迭代器)是一种"能够迭代某序列内所有元素"的对象,可通过改变自寻常pointer的一致性接口来完成工作。Iterator奉行一个纯抽象概念:任何东西,只要行为类似iterator,就是一种iterator。然而不同的的iterator具有不同的行进能力。

迭代器种类

迭代器种类 能力 提供者
Output 迭代器 向前写入 Ostream,inserter
Input 迭代器 向前读取一次 Istream
Forward 迭代器 向前读取 Forward list、unordered containers
Bidirectional 迭代器 向前和向后读取 List、set、multiset、map、multimap
Random-access 迭代器 以随机访问方式读取 Array、vector、deque、string、C-style array
迭代器种类

Output迭代器

Output迭代器允许一步一步前行并搭配write动作。因此你可以一个一个元素地赋值,不能使用output迭代器对同一区间迭代两次。事实上,甚至不保证你可以将一个value复制两次而其迭代器不累进。我们的目标是将一个value以下列形式写入一个黑洞。

1
2
3
4
while(...) {
*pos = ...;
++pos;
}

Output 迭代器无需比较操作。你无法检验output迭代器是否有效,或写入是否成功。你唯一可做的就是写入。通常,一批写入动作是以一个"额外条件定义出"的"特定output迭代器"作为结束。
见下表Output迭代器操作

表达式 效果
*iter = val 将val写至迭代器所指的位置
++iter 向前步进(step forward), 返回新位置
iter++ 向前步进(step forward), 返回旧位置
TYPE(iter) 复制迭代器(copy 构造函数)

通常,迭代器可用来读,也可用来写; 几乎所有reading迭代器都有write的额外功能,这种情况下他们被称为mutable(可产生变化的)迭代器。
一个典型的pure output迭代器例子是:“将元素写至标准输出设备”。 如果采用两个output迭代器写至屏幕, 第二个字将跟在第一个字后面,而不是覆盖第一个字。另一个典型的例子是inserter, 那是一种用来将他插入容器。如果随后写入第二个value, 并不会覆盖第一个value, 而是安插进去。

Input迭代器

Input迭代器只能一次一个以前行方向读取元素,按此顺序一个个返回元素值。
Input迭代器的各项操作

表达式 效果
*iter 读取实际元素
iter->member 读取实际元素的成员(如果有的话)
++iter 向前步进(step forward), 返回新位置
iter++ 向前步进(step forward), 返回旧位置
iter1 == iter2 判断两个迭代器是否相等
iter1 != iter2 判断两个迭代器是否不相等
TYPE(iter) 复制迭代器(copy 构造函数)

Input迭代器只能读取元素一次。如果你复制input迭代器, 并令原input迭代器和新产生的拷贝都向前读取, 可能会遍历到不同的值。
所有的迭代器都具备input迭代器的能力,而且往往更强。Pure input迭代器的典型例子就是"从标准输入设备读取数据"。同一个值不会被读取两次。一旦从input stream读入一个字(离开input缓冲区), 下次读取时就会返回另一个字。

对于input迭代器, 操作符==和!=只用来检查"某个迭代器是否等于一个past-the-end迭代器(指指向最末元素的下一个位置)".这有其必要, 因为处理input迭代器的操作函数通常会有以下行为。

1
2
3
4
5
6
InputIterator pos, end;

while (pos != end) {
... // read-only access using *pos
++pos;
}

没有任何保证说,两个迭代器如果都不是past-the-end迭代器, 且指向不同位置,他们的比较结果会不相等(这个条件是和forward迭代器搭配引入的)。

也请注意, input迭代器的后置式递增操作符(++iter)不一定会返回什么东西。不过通常它会返回旧位置。
你应该尽可能优先先选用前置式递增操作符(++iter)而非后置式递增操作符(iter++), 因为前者效能更好。因为后者会返回一个临时对象。

Forward(前向)迭代器

Forward迭代器是一种input迭代器且在前进读取时提供额外保证。

表达式 效果
*iter 访问实际元素
iter->member 访问实际元素的成员
++iter 向前步进(返回新位置)
iter++ 向前步进(返回旧位置)
iter1 == iter2 判断两个迭代器是否相等
iter1 != iter2 判断两个迭代器是否不等
TYPE() 创建迭代器(default构造函数)
TYPE(iter) 复制迭代器(拷贝构造函数)
iter1 = iter2 对迭代器赋值(assign)
和input迭代器不同的是, 两个forward迭代器如果指向同一元素, operator==会获得true, 如果两者都递增, 会再次指向同一元素。
例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
ForwardIterator pos1, pos2;

pos1 = pos2 = begin; /// both iterator refer to the same element
if(pos1 != end) {
++pos1; /// pos1 is one element ahead
while(pos1 != end) {
if(*pos1 == *pos2) {
... // precess adjacent duplicates
++pos1;
++pos2;
}
}
}

Forward迭代器由以下对象和类型提供:

  • Class<forward_list>
  • Unordered container
    然而标准库也允许unordered容器的实现提供bidirectional迭代器。
    如果forward迭代器履行了output迭代器应有的条件, 那么它就是一个mutable forward迭代器, 即可用于读取,也可用于涂写。

Bidirectional(双向)迭代器

Bidirectional迭代器在forward迭代器的基础上增加回头迭代(iterate backward)能力。

Bidirectional 迭代器的新增操作

表达式 效果
–iter 步退(返回新位置)
iter– 步退(返回旧位置)

Bidirectional迭代器由以下的对象和类型提供:

  • Class list<>.
  • Associative(关联式) 关联式容器提供

如果bidirectional迭代器履行了output迭代器应有的条件, 那么他就是个mutable bidirectional迭代器, 即可用于读取, 也可用于涂写。

Random-Access(随机访问)迭代器

Random-access迭代器在bidirectional迭代器的基础上增加了随机访问能里。因此它必须提供iterator算数运算。也就是说,它能增减某个偏移量、
计算距离(difference), 并运用诸如<和>等管理操作符(relational operator)进行比较。
随机访问迭代器的新增操作:

表达式 效果
iter[n] 访问索引位置为n的元素
iter+=n 前进n个元素(如果n是负数, 则改为回退)
iter-=n 回退n个元素(如果n是负数, 则改为前进)
iter+n 返回iter之后的第n个元素
n+iter 返回iter之后的第n个元素
iter-n 返回iter之前的第n个元素
iter1-iter2 返回iter1和iter2之间的距离
iter1 < iter2 判断iter1是否在iter2之前
iter1 > iter2 判断iter1是否在iter2之后
iter1 <= iter2 判断iter1是否不在iter2之后
iter1 >= iter2 判断iter1是否不在iter2之前

Random-access迭代器由以下对象和类型提供:

  • 可随机访问的容器(arrayvectordeque)
  • String(stringwstring)
  • 寻常的C-Style(pointer)

迭代器相关辅助函数

std::advance()

std::advance()可将迭代器的位置增加, 增加的幅度由实参决定, 也就是说它令迭代器一次前进(或后退)多个元素:

1
2
#include <iterator>
void advance(InputIterator& pos, Dist n)
  • 令名称为pos的input迭代器前进(或后退)n个元素
  • bidirectinal迭代器和random-access迭代器而言, n可为负值, 表示后退
  • Dist是个template类型。通常它必须是个整数类型, 因为会调用诸如<++--等操作, 还要和0做比较。
  • std::advance()并不检查迭代器是否超过序列的end()(因为迭代器通常不知道其所操作的容器, 因此并无检查)。所以, 调用std::advance()有可能导致不明确行为–因为"对序列尾端调用operator++"是一种未定义的行为。

对于random-access迭代器, 此函数只是简单地调用pos+=n, 因此具有常量复杂度。 对于其他任何类型的迭代器, 则调用++pos(或--pos如果n为负值)n次。因此,对于其他任何类型地迭代器, 本函数具有线性复杂度。
如果你希望你的程序可以轻松地更换容器和迭代器种类, 你应该使用std::advance()而不是operator+=
另外, 请注意std::advance()不具有返回值, 而operator+=会返回新位置, 所以后者可作为更大表达式的一部分。

下面是一个std::advance()的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/// 输入迭代器的情况
template <class _InIt, class _Diff>
inline void advance_impl(_InIt& _Where, _Diff _Off, std::input_iterator_tag) {
/// 检查该偏移量不能为负值
if (_Off < 0) {
assert(false && "negative offset in advance");
}
/// 使用自增运算符来计算
for (; 0 < _Off; --_Off) ++_Where;
}

/// 双向迭代器
template <class _BidIt, class _Diff>
inline void advance_impl(_BidIt& _Where, _Diff _Off,
std::bidirectional_iterator_tag) {
/// 使用自增运算符来计算
for (; 0 < _Off; --_Off) ++_Where;
/// 如果偏移量为负值则使用自减运算符
for (; _Off < 0; ++_Off) --_Where;
}

/// 随机访问迭代器
template <class _RanIt, class _Diff>
inline void advance_impl(_RanIt& _Where, _Diff _Off,
std::random_access_iterator_tag) {
/// 使用operator += ,常量复杂度
_Where += _Off;
}

template <class _InIt, class _Diff>
inline void advance(_InIt& _Where, _Diff _Off) {
advance_impl(_Where, _Off,
/// 在萃取迭代器的特性时去掉其const的属性来提高性能
std::iterator_traits<_Iter>::iterator_category<
std::remove_const_t<_InIt>>());
}

std::next()和std::prev()

c++ 提供了两个新增的辅助函数, 允许你前进和后退移动迭代器的位置。

1
2
3
#include <iterator>
ForwardIterator next(ForwardIterator pos)
ForwardIterator next(ForwardIterator pos, Dist n)
  • 导致forward迭代器pos前进或n个位置
  • 如果处理的是bidirectionalrandom-access迭代器, n可为负值, 导致后退移动
  • Dist是类型std::iterator_traits<ForwardIterator>::difference_type
  • 其内部将对一个临时对象调用std::advance(pos, n)
  • 注意, std::next()并不检查是否会跨越序列的end()。因此调用者必须自行担保其结果有效。
1
2
3
#include <iterator>
BidirectionalIterator prev(BidirectionalIterator pos)
BidirectionalIterator prev(BidirectionalIterator pos, Dist n)
  • 导致bidirectional迭代器pos后退一个或n个位置
  • n可为负值, 导致向前移动
  • Dist是类型std::iterator_traits<ForwardIterator>::difference_type
  • 其内部将对一个临时对象调用std::advance(pos, -n)
  • 注意, std::prev()并不检查是否会跨越序列的begin()。因此调用者必须自行担保其结果有效。

下面写一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template <class _InIt>
inline _InIt next(
_InIt _First,
std::iterator_traits<_InIt>::iterator_category<_InIt> _Off = 1) {
static_assert(
std::is_base_of<
std::input_iterator_tag,
typename std::iterator_traits<_InIt>::iterator_category>::value,
"next requires input iterator");

advance(_First, _Off);
return (_First);
}

template <class _BidIt>
inline _BidIt prev(
_BidIt _First,
std::iterator_traits<_BidIt>::iterator_category<_BidIt> _Off = 1) {
static_assert(
std::is_base_of<
std::bidirectional_iterator_tag,
typename std::iterator_traits<_BidIt>::iterator_category>::value,
"prev requires bidirectional iterator");

advance(_First, -_Off);
return (_First);
}

std::distance()

std::distance()用来处理两个迭代器之间的距离:

1
Dist distance(InputIterator pos1, InputIterator pos2)
  • 返回两个input迭代器pos1pos2之间的距离。
  • 两个迭代器必须指向同一个容器
  • 如果不是random-access迭代器, 则从pos1开始前进必须能够到达pos2, 亦即pos2的位置必须与pos1相同或在其后。
  • 返回类型Dist是类型std::iterator_traits<ForwardIterator>::difference_type

注意: 处理两个non-random-access迭代器之间的距离时, 必须十分小心。第一个迭代器所指的元素绝不能在第二个迭代器所指元素之后方, 否则会导致不明确的行为。如果不知道哪个迭代器在前, 你必须先算出两个迭代器分别至容器起点的距离, 在根据这两个距离来判断。

一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template <typename it>
typename std::iterator_traits<it>::difference_type distance(it from, it to) {
if constexpr (typename std::iterator_traits<it>::iterator_category() ==
std::random_access_iterator_tag) {
/// 随机访问迭代器
return to - from;
} else if constexpr (typename std::iterator_traits<it>::iterator_category() ==
std::input_iterator_tag) {
/// input 迭代器
typename std::iterator_traits<it>::difference_type res = 0;
for (; from != to; ++from) {
++res;
}
return res;
} else {
static_assert("unknow iterator type.");
}
}

std::iter_swap()

这个简单的辅助函数用来交换两个迭代器所指的元素值

1
2
#include <algorithm>
void iter_swap(ForwardIterator1 pos1, ForwardIterator pos2)
  • 交换迭代器pos1和pos2所指的值
  • 迭代器的类型不必相同, 但其所指的两个值必须可以相互赋值

integer_sequence

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <array>
#include <iostream>
#include <tuple>
#include <utility>

template <typename T, T... ints>
void print_sequence(std::integer_sequence<T, ints...> int_seq) {
std::cout << "" << int_seq.size() << ": ";
((std::cout << ints << ' '), ...);
std::cout << '\n';
}

/// 转化数组为tuple
template <typename Array, std::size_t... I>
auto a2t_impl(const Array& a, std::index_sequence<I...>) {
return std::make_tuple(a[I]...);
}

template <typename T, std::size_t N,
typename Indices = std::make_index_sequence<N>>
auto a2t(const std::array<T, N>& a) {
return a2t_impl(a, Indices{});
}

/// 漂亮地打印 tuple
template <class Ch, class Tr, class Tuple, std::size_t... Is>
void print_tuple_impl(std::basic_ostream<Ch, Tr>& os, const Tuple& t,
std::index_sequence<Is...>) {
((os << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}

template <class Ch, class Tr, class... Args>
auto& operator<<(std::basic_ostream<Ch, Tr>& os, const std::tuple<Args...>& t) {
os << "(";
print_tuple_impl(os, t, std::index_sequence_for<Args...>{});
return os << ")";
}

int main() {
print_sequence(std::integer_sequence<unsigned, 9, 2, 5, 1, 9, 1, 6>{});
print_sequence(std::make_integer_sequence<int, 20>{});
print_sequence(std::make_index_sequence<10>{});
print_sequence(std::index_sequence_for<float, std::iostream, char>{});

std::array<int, 4> array = {1, 6, 3, 4};

auto tuple = a2t(array);
static_assert(std::is_same<decltype(tuple), std::tuple<int, int, int, int>>::value, "");

std::cout << tuple <<'\n';
system("pause");
}

正确写法

该写法在第一次调用get_instance()后构造该实例,线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class object{
public:
~object() = default;
static std::unique_ptr<object>& get_instance();
private:
object() = default; ///< 构造函数写为private,防止其他调用者单独构造该对象实例。
};

std::unique_ptr<object>& object::get_instance();
static std::unique_ptr<object> instance; ///< 该对象的唯一实例
static std::once_flag flag; ///< 标志位, 标记只调用一次
std::call_once(flag, [&](){
instance = std::make_unique<object>(); ///< C++14以后版本的方法
/*
instance = std::unique_ptr<object>(new object()); ///< C++14 到 C++11 可用的方法
*/
});
return instance;
}

其他写法比较

最简单的写法: 线程不安全
由于new object()这个构造的过程需要时间,所以可能造成两个线程同时获取到instance变量为空指针。从而导致实例化两次,从未导致硬件驱动加载两次,而导致崩溃。

1
2
3
4
5
6
7
object* object::get_instance() {
object* instance = nullptr;
if(instance == nullptr) {
instance = new object();
}
return instance;
}

double check写法: 看起来线程安全,其实有条件竞争。
#1#2处,可能发生一个线程正在对instance变量赋值(写操作), 而另一个线程在进行在进行判断instance变量是否为空(读操作),从而导致条件竞争,而导致崩溃。

1
2
3
4
5
6
7
8
9
10
11
object* object::get_instance(){
static std::mutex mt;
volatile object* instance = nullptr; ///< volatile关键字为了防止编译器优化
if(instance == nullptr) { ///< #1 读操作
std::lock_guard<std::mutex> lock(mt);
if(instance == nullptr) {
instance = new object(); ///< #2 写操作
}
}
return instance;
}

官网解释

Qt::ConnectionType

跨线程的信号和槽
Qt支持这些信号槽的连接方式:

  1. Auto Connection(默认): 假如信号在一个接收者的线程中发射,则行为等同于 Direct Connect. 否则行为等同于Queued Connection.
  2. Direct Connect: 当信号被发射,槽函数将会被立即调用。槽函数将会在发射者的线程中执行, 而不一定在接收者线程中执行。
  3. Queued Connect: 槽函数在控制权返回到接收者线程的事件循环时被调用。槽函数在接收者线程中被执行。
  4. Blocking Queued Connection: 槽函数除了阻塞当前线程直到槽函数返回,其他像Queued Connection一样被调用。备注:在同一线程中使用这个类型的connect会导致死锁。
  5. Unique Connect: 这个行为等同于Auto Connection,但是这个connection是只能在现有连接不重复的情况下生效。假如相同的信号已经连接到相同的槽函数中,这个连接不会建立且connect()返回false
  • 连接的类型可以通过connect()额外的参数指定,注意:在发送者和接收者在不同线程中使用direct connect是不安全的。如同一个事件循环在接收者的线程中,在另一个线程中调用存活对象的任何函数是不安全的。
  • QObject::connect() 它本身是线程安全的。

在使用Queue Connection的时候,参数必须是Qt 元对象系统已知的类型,因为Qt需要拷贝入参并保存在事件背后的场景。假如你使用Queue Connection并得到以下错误信息:

QObject::connect: Cannot queue arguments of type ‘MyType’

在connection建立之前,调用qRegisterMetaType()去注册数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <iostream>

template <typename T>
class queue {
public:
queue(size_t size) : size_(size), front_(0), end_(0) { data_ = new T[size]; }
~queue() { delete[] data_; }
queue(const queue& other) = delete;
queue(queue&& other) = delete;
queue operator=(const queue& other) = delete;
queue operator=(queue&& other) = delete;

bool is_empty() { return front_ == end_; }
bool is_full() { return front_ = (end_ + 1) % size_; }

const T& front() const { return data_[front_]; }

void push(const T& val) {
if ((end_ + 1) % size_ != front_) {
data_[end_] = val;
end_ = (end_ + 1) % size_;
}
}

void pop() {
if (front_ != end_) {
front_ = (front_ + 1) % size_;
}
}

private:
size_t front_;
size_t end_;
size_t size_;
T* data_;
};

int main() {
queue<int> q(5);
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
q.pop();
q.pop();
q.push(6);
q.push(7);
q.push(8);
while (!q.is_empty()) {
std::cout << q.front() << std::endl;
q.pop();
}
return 0;
}

不采用哨兵值,使用状态来实现

spdlog 循环队列实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
template <typename T>
class circular_q {
size_t max_items_ = 0;
typename std::vector<T>::size_type head_ = 0;
typename std::vector<T>::size_type tail_ = 0;
size_t overrun_counter_ = 0;
std::vector<T> v_;

public:
using value_type = T;

// empty ctor - create a disabled queue with no elements allocated at all
circular_q() = default;

explicit circular_q(size_t max_items)
: max_items_(max_items + 1) // one item is reserved as marker for full q
,
v_(max_items_) {}

circular_q(const circular_q &) = default;
circular_q &operator=(const circular_q &) = default;

// move cannot be default,
// since we need to reset head_, tail_, etc to zero in the moved object
circular_q(circular_q &&other) noexcept {
copy_moveable(std::move(other));
}

circular_q &operator=(circular_q &&other) noexcept {
copy_moveable(std::move(other));
return *this;
}

// push back, overrun (oldest) item if no room left
void push_back(T &&item) {
if (max_items_ > 0) {
v_[tail_] = std::move(item);
tail_ = (tail_ + 1) % max_items_;

if (tail_ == head_) // overrun last item if full
{
head_ = (head_ + 1) % max_items_;
++overrun_counter_;
}
}
}

// Return reference to the front item.
// If there are no elements in the container, the behavior is undefined.
const T &front() const { return v_[head_]; }

T &front() { return v_[head_]; }

// Return number of elements actually stored
size_t size() const {
if (tail_ >= head_) {
return tail_ - head_;
} else {
return max_items_ - (head_ - tail_);
}
}

// Return const reference to item by index.
// If index is out of range 0…size()-1, the behavior is undefined.
const T &at(size_t i) const {
assert(i < size());
return v_[(head_ + i) % max_items_];
}

// Pop item from front.
// If there are no elements in the container, the behavior is undefined.
void pop_front() { head_ = (head_ + 1) % max_items_; }

bool empty() const { return tail_ == head_; }

bool full() const {
// head is ahead of the tail by 1
if (max_items_ > 0) {
return ((tail_ + 1) % max_items_) == head_;
}
return false;
}

size_t overrun_counter() const { return overrun_counter_; }

private:
// copy from other&& and reset it to disabled state
void copy_moveable(circular_q &&other) noexcept {
max_items_ = other.max_items_;
head_ = other.head_;
tail_ = other.tail_;
overrun_counter_ = other.overrun_counter_;
v_ = std::move(other.v_);

// put &&other in disabled, but valid state
other.max_items_ = 0;
other.head_ = other.tail_ = 0;
other.overrun_counter_ = 0;
}
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <algorithm>
#include <atomic>
#include <cassert>
#include <iostream>
#include <map>
#include <mutex>
#include <vector>

class Container {
public:
Container() {
std::lock_guard<std::mutex> lock(mut);
auto it = uuid.load();
uuid.store(++it);
serial_number_ = it;
container_map.insert({ serial_number_, this });
}

virtual int get_fluid_capacity() = 0;

friend int totalFluidCapacity() {
std::lock_guard<std::mutex> lock(mut);
int result = 0;
for (auto& it : container_map) {
result += it.second->get_fluid_capacity();
}
return result;
}

friend Container* find_container(size_t serial_number) {
std::lock_guard<std::mutex> lock(mut);
if(container_map.find(serial_number) != container_map.end()) {
return container_map[serial_number];
} else {
return nullptr;
}
}
inline static std::mutex mut;
inline static std::atomic<size_t> uuid;
inline static std::map<size_t, Container*> container_map;

protected:
size_t serial_number_;
};

int totalFluidCapacity();
Container* find_container(size_t serial_number);

class Buckets : public Container {
static constexpr double pi = 3.1415926;

public:
Buckets(int height, int radius) : height_(height), radius_(radius) {}
virtual int get_fluid_capacity() override final {
return static_cast<int>(radius_ * radius_ * height_ * pi);
}

private:
int height_;
int radius_;
};

class Boxes : public Container {
public:
enum class material_type : int {
m = 0, /// for metal
c = 1, /// for cardboard
};

Boxes(const int length, const int width, const int height, material_type type)
: length_(length), width_(width), height_(height), type_(type) {}
virtual int get_fluid_capacity() override final {
if (type_ == material_type::c) {
return length_ * width_ * height_;
} else {
return 0;
}
}

private:
int length_;
int width_;
int height_;
material_type type_;
};

int main() {
Boxes box_c(1, 1, 1, Boxes::material_type::c);
Boxes box_m(1, 1, 1, Boxes::material_type::m);

Buckets bucket(1, 1);

auto capa = totalFluidCapacity();
assert(capa == 4);

auto p_container = find_container(2);
assert(p_container == & box_m);
return 0;
}

C++序列化与反序列化二叉树

首先我们定义树节点的数据结构

1
2
3
4
5
6
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

序列化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
inline std::string serialize(TreeNode* root) {
if (root == nullptr) {
return "null";
}
std::vector<TreeNode*> queue;
std::vector<std::vector<std::string>> result;
queue.push_back(root);
int height = 0;
while (!queue.empty() &&
!std::all_of(queue.begin(), queue.end(),
[](const TreeNode* node) { return node == nullptr; })) {
std::vector<std::string> temp;
int count = static_cast<int>(std::pow(2, height));
while (count-- != 0) {
auto front = queue.front();
queue.erase(queue.begin());
if (front == nullptr) {
temp.push_back("null");
queue.push_back(nullptr);
queue.push_back(nullptr);
}
else {
temp.push_back(std::to_string(front->val));
queue.push_back(front->left);
queue.push_back(front->right);
}
}
result.push_back(temp);
++height;
}

std::string str = "[";
/// 组装字符串
for (auto& it : result) {
for (auto& it1 : it) {
str += it1 + ",";
}
}
if (str.back() == ',') {
str.erase(std::prev(str.end()));
str += "]";
}
return str;
}

反序列化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
inline void split(const std::string& s, std::vector<std::string>& tokens,
const std::string& delimiters = " ") {
std::string::size_type lastPos = s.find_first_not_of(delimiters, 0);
std::string::size_type pos = s.find_first_of(delimiters, lastPos);
while (std::string::npos != pos || std::string::npos != lastPos) {
tokens.push_back(s.substr(lastPos, pos - lastPos));
lastPos = s.find_first_not_of(delimiters, pos);
pos = s.find_first_of(delimiters, lastPos);
}
}
// Decodes your encoded data to tree.
inline TreeNode* deserialize(std::string data) {
if (data.front() != '[' || data.back() != ']') {
return nullptr;
}
data.erase(data.begin());
data.erase(std::prev(data.end()));
std::vector<std::string> vec_str;
split(data, vec_str, ",");
if (vec_str.empty() || vec_str.front() == "null") {
return nullptr;
}

std::vector<TreeNode*> result;
for (const auto& it : vec_str) {
if (it == "null") {
result.push_back(nullptr);
}
else {
result.push_back(new TreeNode(std::stoi(it)));
}
}

for (size_t i = 0; i < result.size(); ++i) {
if(result.at(i) != nullptr){
if (result.size() > i * 2 + 2) {
result.at(i)->left = result.at(i*2+1);
result.at(i)->right = result.at(i*2+2);
}
}
}

return result.at(0);
}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <cassert>

int main(){

auto result = deserialize("[5,null,7,null,null,6,8]");
assert(serialize(result) == "[5,null,7,null,null,6,8]");

auto string =
"[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,"
"27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,"
"51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,"
"75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,"
"99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,"
"117,118,119,120,121,122,123,124,125,126]";
auto result3 = deserialize(string);
assert(serialize(result3) == string);
}

序列化改进版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Codec {
public:
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
std::string res = "[";
std::queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
auto temp = q.front();
q.pop();
if (temp == nullptr) {
res += "null,";
} else {
res += std::to_string(temp->val) + ",";
q.push(temp->left);
q.push(temp->right);
}
}
res += "]";
return res;
}

// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
/// 去除首尾的'[]'
data.erase(data.begin());
data.erase(std::prev(data.end()));
/// 把字符串解析成数组
auto begin = 0;
auto lastPos = data.find_first_not_of(',', 0);
auto pos = data.find_first_of(',', lastPos);
std::queue<std::string> q;
while (pos != std::string::npos || std::string::npos != lastPos) {
q.push(data.substr(lastPos, pos - lastPos));
lastPos = data.find_first_not_of(',', pos);
pos = data.find_first_of(',', lastPos);
}
/// 遍历数组建立树
auto head = q.front();
std::queue<TreeNode*> q_ceng;
std::queue<TreeNode*> q_next_ceng;
TreeNode* r = nullptr;
if (head != "null") {
auto thead = new TreeNode(std::atoi(head.c_str()));
q_ceng.push(thead);
r = thead;
}
q.pop();
while (!q.empty()) {
while (!q_ceng.empty()) {
auto t = q_ceng.front();
q_ceng.pop();
auto l_t = q.front();
q.pop();
auto r_t = q.front();
q.pop();
if (l_t != "null") {
t->left = new TreeNode(std::atoi(l_t.c_str()));
q_next_ceng.push(t->left);
}
if (r_t != "null") {
t->right = new TreeNode(std::atoi(r_t.c_str()));
q_next_ceng.push(t->right);
}
}
while (!q_next_ceng.empty()) {
q_ceng.push(q_next_ceng.front());
q_next_ceng.pop();
}
}
return r;
}
};

源码链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <map>
#include <string>
#include <vector>

/**
* @brief 把字符串前后的字符串给去除
* @param s [in] 要剪切的字符串
* @param chars [in] 要去除什么的字符串
* @return std::string& 剪切后的字符串
* @author lijiancong(lijiancong@gbcom.com.cn)
* @note
*/
static std::string& strip(std::string& s, const std::string& chars = " ") {
s.erase(0, s.find_first_not_of(chars.c_str()));
s.erase(s.find_last_not_of(chars.c_str()) + 1);
return s;
}

/**
* @brief 以特定符号为分隔符,切分字符串并放入vector里
* @param s [in] 原字符串
* @param tokens [out] 剪切后的子字符串
* @param delimiters [in] 分隔符
* @author lijiancong(lijiancong@gbcom.com.cn)
* @note
*/
static void split(const std::string& s, std::vector<std::string>& tokens,
const std::string& delimiters = " ") {
std::string::size_type lastPos = s.find_first_not_of(delimiters, 0);
std::string::size_type pos = s.find_first_of(delimiters, lastPos);
while (std::string::npos != pos || std::string::npos != lastPos) {
tokens.push_back(s.substr(lastPos, pos - lastPos));
lastPos = s.find_first_not_of(delimiters, pos);
pos = s.find_first_of(delimiters, lastPos);
}
}

static void parse(std::string& s, std::map<std::string, std::string>& items) {
std::vector<std::string> elements;
s.erase(0, s.find_first_not_of(" {"));
s.erase(s.find_last_not_of("} ") + 1);
split(s, elements, ",");
for (auto& iter : elements) {
std::vector<std::string> kv;
split(iter, kv, ":");
if (kv.size() != 2) continue;
items[strip(kv[0], " \"")] = strip(kv[1], " \"");
}
}

int main() {
std::string data =
" { \"key1\" : \"data1\" , \"key2\" : \"data2\" } ";
std::map<std::string, std::string> items;
parse(data, items);

for (const auto& iter:items){
std::cout << "key=" << iter.first << ",value=" << iter.second << std::endl;
}
system("pause");
}

thread_pool 源码学习

rotating_file_sink定义

rotating_file_sink.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//
// Rotating file sink based on size
//
template<typename Mutex>
class rotating_file_sink final : public base_sink<Mutex>
{
public:
rotating_file_sink(filename_t base_filename, std::size_t max_size, std::size_t max_files, bool rotate_on_open = false);
static filename_t calc_filename(const filename_t &filename, std::size_t index);
filename_t filename();

protected:
void sink_it_(const details::log_msg &msg) override;
void flush_() override;

private:
// Rotate files:
// log.txt -> log.1.txt
// log.1.txt -> log.2.txt
// log.2.txt -> log.3.txt
// log.3.txt -> delete
void rotate_();

// delete the target if exists, and rename the src file to target
// return true on success, false otherwise.
bool rename_file_(const filename_t &src_filename, const filename_t &target_filename);

filename_t base_filename_; ///< 基础文件名称
std::size_t max_size_; ///< 最大单个文件大小
std::size_t max_files_; ///< 最大日志文件数量
std::size_t current_size_; ///< 当前文件的大小
details::file_helper file_helper_; ///< 用于辅助写文件的对象
};

构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

/// @name rotating_file_sink
/// @brief 构造本对象,
/// 1. 打开日志文件
/// 2. 获取当前文件大小
/// 3. 如果超出了单个文件大小,则重新创建文件并打开
///
/// @param base_filename [in] 基础的文件名
/// @param max_size [in] 最大单个文件大小
/// @param max_files [in] 最大的文件
/// @param rotate_on_open [in] 是否在文件打开时rotate
///
/// @date 2020-07-04 21:51:27
rotating_file_sink(std::string base_filename, std::size_t max_size,
std::size_t max_files, bool rotate_on_open = false)
: base_filename_(std::move(base_filename)),
max_size_(max_size),
max_files_(max_files) {
/// 打开当前应该写入的文件,并由file_helper对象来持有这个文件指针
file_helper_.open(calc_filename(base_filename_, 0));
/// 该函数时间执行时间很长,在这里只执行一次。
current_size_ = file_helper_.size();
/// 假如允许rotate且当前文件大小大于零
if (rotate_on_open && current_size_ > 0) {
/// 执行一次rotate
rotate_();
}
}

rotate_()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/// @name     rotate_
/// @brief 执行循环日志文件的创建
/// @details
/// Rotate files:
/// log.txt -> log.1.txt
/// log.1.txt -> log.2.txt
/// log.2.txt -> log.3.txt
/// log.3.txt -> delete
///
/// @param NONE
///
/// @return NONE
///
/// @date 2020-07-04 21:58:24
inline void rotate_() {
/// 首先关闭该文件
file_helper_.close();
/// 开始查找要创建的下一个日志文件名称
for (auto i = max_files_; i > 0; --i) {
/// 拼装出上一个该文件名称
std::string src = calc_filename(base_filename_, i - 1);
if (!path_exists(src)) {
/// 该文件如果不存在则到下一个循环
continue;
}
/// 这里找到要创建的日志名称了日志文件的名称
std::string target = calc_filename(base_filename_, i);

/// 把上一个文件改名为当前的文件名
if (!rename_file_(src, target)) {
/// 如果失败则在一个短暂的延迟后再次尝试
sleep_for_millis(100);
if (!rename_file_(src, target)) {
/// 关闭并打开这个日志文件,防止它增长超出限制
file_helper_.reopen(true);
current_size_ = 0;
/// 抛出异常
throw("rotating_file_sink: failed renaming ");
}
}
}
/// 以追加模式("a")重新打开这个文件
file_helper_.reopen(true);
}

虚函数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/// @name   sink_it_
/// @brief 写文件日志的函数,如果写入日志大于最大文件大小则创建下一个文件
///
/// @param msg [in] 写入的日志信息
///
/// @return
///
/// @date 2020-07-05 09:33:12
void sink_it_(const details::log_msg &msg) override {
std::string formatted;
/// 拼装日志信息
base_sink<Mutex>::formatter_->format(msg, formatted);
/// 计算这条日志加上原本大小是否超过了最大文件大小
current_size_ += formatted.size();
if (current_size_ > max_size_) {
/// 超过了就创建下一个文件
rotate_();
/// 更新为当前文件大小为这个日志信息的大小
current_size_ = formatted.size();
}
/// 如果没有超过最大大小则继续写该文件
file_helper_.write(formatted);
}

/// @name flush_
/// @brief 刷新文件
///
/// @param NONE
///
/// @return NONE
///
/// @date 2020-07-05 09:36:23
void flush_() override { file_helper_.flush(); }

异常处理

异常处理exception handling)机制允许程序独立开发的部分能够在运行时就出现问题进行通信并作出相应的处理。异常是的我们能够将问题的检测和解决过程分离开来。程序的一部分负责检测问题的出现,然后解决该问题的任务传递给程序的另一部分。检测环节无需知道问题处理模块的所有细节,反之亦然。

1. 抛出异常

​在C++语言中,我们通过抛出(throwing)一条表达式来引发(raised)一个异常。被抛出的表达式的类型以及当前的调用链共同决定了哪段处理代码(handler)将被用来处理该异常。被选中的处理代码实在调用链中与抛出对象类型匹配的最近的处理代码。其中,根据抛出对象的类型和内容,程序的异常抛出部分会告知异常处理部分到底发生了什么错误。

​当执行一个throw时,跟在throw后面的语句将不再被执行。相反,程序的控制权从throw转移到与之匹配的catch模块。该catch可能是同一函数中的局部catch也可能位于直接或间接调用了发生异常的函数的另一个函数中。控制权从一处转移到另一处,这有两个重要的含义:

  • 沿着调用链的函数可能会提早退出
  • 一旦程序开始执行异常处理代码,则沿着调用链创建的对象将被销毁

因为跟在throw后面的语句将不再被执行,所以throw语句的有类似于return语句:它通常作为条件语句的一部分或者作为某个函数的最后(或者唯一)一条语句。

1.1 栈展开

当抛出一个异常后,程序暂停当前函数的执行过程并立即开始寻找与异常匹配的catch子句。

  • throw出现在一个try语句块(try block)内时,检查与该try块关联的catch子句。

  • 如果找到了匹配的catch,就使用该catch处理异常。

  • 如果这一步没找到匹配catch且该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句

  • 如果还是找不到匹配catch,则退出当前函数,在调用当前函数的外层函数中继续寻找。

  • 如果对抛出异常的函数的调用语句位于一个try语句块内,则检查与该try块关联的catch子句。

  • 如果找到了匹配的catch,就使用该catch处理异常。

  • 否则,如果该try语句嵌套在其他try块中,则继续检查与外层try匹配的catch子句。

  • 如果仍然没找到匹配的catch,则退出当前这个主调函数,继续在调用刚刚退出的这个函数的其他函数中寻找,以此类推。

​上述过程被称为栈展开(stack unwinding)过程。栈展开过程沿着嵌套函数的调用链不断查找,直到找到了与异常匹配的catch子句为止;或者也可能一致没找到匹配的catch,则退出主函数后过程中止。

​假设找到了一个匹配的catch子句,则程序进入该子句并执行其中代码。当执行完这个catch子句后,找到与try块关联的最后一个catch子句后的点,并从这里继续执行。

如果没有找到匹配的catch子句,程序将退出。因为异常通常被认为是妨碍程序正常执行的事件,所以一旦引发了某个异常,就不能对它置之不理。当找不到匹配的catch时,程序将调用标准库函数terminate,顾名思义,terminate负责中止程序的执行过程。

1.2 栈展开过程中对象被自动销毁

​在栈展开过程中,位于调用链上的语句块可能会提前退出。如果在栈展开过程中退出了某个块,编译器将负责确保在这个块中创建的对象都能被正确的销毁。如果某个局部对象的类型是类类型,则该对象的析构函数将被自动调用。与往常一样,编译器在销毁内置类型的对象时不需要做任何事情。

​如果异常发生在构造函数中,则当前的对象可能只构造了一部分。有的成员已经开始初始化了,而另外一些成员在异常发生前也许还没有开始初始化。即使某个对象只构造了一部分,我们也要确保构造的成员能被正确的销毁(否则会发生内存泄露)。

​类似的,异常也可能发生在数组标准库容器的元素初始化过程中。与之前类似,如果在异常发生前已经构造了一部分元素,则我们应该确保这部分元素被正确的销毁。

1.3 析构函数与异常

​析构函数总是会被执行的,但是函数中负责释放资源的代码却可能会被跳过。如果一个块分配了资源,并且在负责释放这些资源的代码前面发生了异常,则释放资源的代码将不会被执行。另一方面,类对象分配的资源将由类的析构函数负责释放。因此,如果我们使用类来控制资源的分配,就能确保无论函数正常结束还是遭遇异常,资源都能被正确地释放。(RAII的思想,在构造函数中获取资源(i.e new),在析构函数中释放资源(i.e delete)。)

​所以出于栈展开可能使用析构函数的考虑,析构函数不应该抛出不能被它自身处理的异常。换句话说,如果析构函数需要执行某个可能抛出异常的操作,则该操作应该被放置在一个try语句块当中,并且在析构函数内部得到处理(如果不这样做的话,程序会马上被终止)。

注:所有标准库类型都能保证它们的析构函数不会引发异常。

1.4 异常对象

异常对象exception object)是一种特殊的对象,编译器使用异常抛出表达式来对异常对象进行拷贝初始化。因此throw语句中的表达式必须拥有完整类型。而且如果该表达式是类类型的话,则相应的类必须含有一个可访问的析构函数和一个可访问的拷贝或移动构造函数。如果该表达式是数组类型函数类型,则表达式将被转换成与之对应的指针类型

​异常对象位于有编译器管理的空间中,编译器确保无论调用哪个catch子句都能访问该空间。异常处理完毕后,异常对象被销毁。

​当一个异常被抛出是,沿着调用链的块将依次退出直至找到与异常匹配的处理代码。如果退出某个块,则同时释放块中局部对象使用的内存。因此,抛出一个指向局部对象的指针几乎肯定是一种错误行为。如果指针所指的对象位于某个块中,而该块在catch语句之前就已经退出了,则意味着在执行catch语句之前局部对象已经被销毁了。

​当我们抛出一条表达式时,该表达式的静态编译时类型决定了异常对象的类型。很多情况下程序抛出的表达式类型来自于某个继承体系。如果一条throw表达式解引用一个基类指针,而该指针实际指向的是派生类对象,则抛出的对象将被切掉一部分,只有基类部分被抛出

注: 抛出指针要求在任何对应处理代码存在的地方,指针所指的对象都必须存在。

2. 捕获异常

catch子句(catch clause)中的一场声明(exception declaration)看起来像是只包含一个形参的函数形参列表。像在形参列表中一样,如果catch无须访问抛出的表达式的话,则我们可以忽略捕获形参的名字。

声明的类型决定了处理代码所能捕获的异常类型。这个类型必须是完全类型,它可以是左值引用,不能是右值引用。当进入一个catch语句后,入参通过异常对象初始化异常声明中的参数。和函数的参数类似,如果catch的参数类型是非引用类型,则该参数是异常对象的一个副本,如果参数是引用类型,则和其他引用参数一样,该参数是异常对象的一个别名。

如果catch的参数是基类类型,则我们可以使用其派生类类型的异常对象对其进行初始化。此时,如果catch的参数是非引用类型,则异常对象将被切掉一部分,如果catch的参数是基类的引用,则该参数将以常规方式绑定到异常对象上。

最后一点需要注意的是,异常声明的静态类型将决定catch语句所能执行的操作。如果catch的参数是基类类型,则catch无法使用派生类特有的任何成员。

Tips: 通常情况下,如果catch接收的异常与某个继承体系有关,则最好将该catch的参数定义成引用类型。

2.1 查找匹配的处理代码

​在搜寻catch语句的过程中,我们最终找到的catch未必是异常的最佳匹配。相反,挑选出来的应该是第一个与异常匹配的catch语句。因此,越是专门的catch越应该置于整个catch列表的前端。

​因为catch语句是按照其出现的顺序逐一匹配的,所以当程序员使用具有继承关系的多个异常时必须对catch语句的顺序进行组织管理,是的派生类异常的处理代码出现在基类异常的处理代码异常之前。

​与实参和形参的匹配规则相比,异常和catch异常声明的匹配规则受到更多限制。此时,绝大多数类型转换都不被允许,除了一些极细小的差别之外,要求异常的类型和catch声明的类型时精确匹配的:

  • 允许从非常量的类型转换,也就是说一条非常量对象的throw语句可以匹配一个接受常量引用的catch语句
  • 允许从派生类向基类的类型转换。
  • 数组被转换成指向数组(元素)类型的指针,函数被转化成指向该函数类型的指针。

除此之外,包括标准算术类型转换和类类型转换在内,其他所有转换规则都不能在匹配catch的过程中使用。

如果在多个catch语句的类型之间存在着继承关系,则我们应该把继承链最低端的类(most derived type)放在前面,而将继承链最顶端的类(least derived type)放在后面。

2.2 重新抛出

​一个单独的catch语句不能完整的处理某个异常。在执行了某些校正操作之后,当前的catch可能会决定由调用链更上一层的函数接着处理异常。一条catch语句通过重新抛出的操作将异常传递给另外一个catch语句。这里的重新抛出仍然是一条throw语句,只不过不包含任何表达式: throw;

​空的throw语句只能出现在catch语句或catch语句直接或间接调用的函数之内。如果在处理代码之外的区域遇到了空throw语句,编译器将调用terminate

​一个重新抛出语句并不指定新的表达式,而是将当前的异常对象沿着调用链向上传递。

​很多时候,catch语句会改变其参数内容。如果在改变了参数的内容后catch语句重新抛出异常,则只有当catch异常声明是引用类型时我们对参数所作的改变才会被保留并继续传播。

2.3 捕获所有异常的处理代码

​为了一次性捕获所有异常,我们使用省略号作为异常声明,这样的处理代码称为捕获所有异常的处理代码,形如catch(...).

catch(...)通常与重新抛出语句一起使用,其中catch执行当前局部能完成的工作,随后重新抛出异常。

Tips: 如果catch(...)与其他几个catch语句一起出现,则catch(...)必须在最后的位置。出现在捕获所有一场语句后面的catch语句将永远不会被匹配。

3. 函数try语句块与构造函数

​通常情况下,程序执行的任何时刻都可能发生异常,特别是一场可能发生在处理构造函数初始值的过程中。构造函数在进入其函数体之前首先执行初始值列表。因为在初始值列表抛出异常时构造函数体内的try语句块还未生效,所以构造函数体内的catch语句无法处理构造函数初始值列表抛出的异常。

​要想处理构造函数初始值抛出的异常,我们必须将构造函数写成函数try语句块function try block)的形式。函数try语句使得一组catch语句既能处理构造函数体(或析构函数体),也能处理构造函数的初始化过程(或析构函数的析构过程)。

1
2
3
4
5
6
7
8
9
template <typename T>
Blob<T>::Blob(std::initializer_list<T> il)
try
: data(std::make_shared<std::vector<T>>(il))
{/** ... */}
catch (const std::bad_alloc &e)
{
handle_out_of_memory(e);
}

4. noexcept 异常说明

1
2
void recoup() noexcept; /** 不会抛出异常 */
void alloc(); /** 可能会抛出异常 */
  • 对于一个函数来说,noexcept说明要么出现在该函数的所有声明语句和定义语句中,要么一次也不出现。该说明应该在函数应该在函数的尾置返回类型之前。

  • 我们也可以在函数指针的声明和定义中指定noexcept

  • 在typedef或类型别名中则不能出现noexcept

  • 在成员函数中,noexcept说明符需要跟在const及引用限定符之后,而在finaloverride或虚函数=0之前。

4.1 违反异常说明

​编译器并不会在编译时检查noexcept说明。实际上,如果一个函数说明了noexcept的同时又含有throw语句或者调用了可能抛出异常的其他函数,编译器将顺利通过,并不会因为这种违反异常说明的情况而报错。

​因此可能会出现一种情况:尽管函数说明了它不会抛出异常,但实际上还是抛出了。一旦一个noexcept函数抛出异常,程序就会调用terminate以确保遵守不在运行时抛出异常的承诺。

​上述过程是执行栈展开未作约定,因此noexcept可以用在两种情况下:一是我们确认函数不会抛出异常,二是我们根本不知道该如何处理异常。

4.2 noexcept运算符

noexcept说明符接受一个可选实参,该实参必须能转换为bool类型:如果实参是true,则函数不会抛出异常;如果实参是false,则函数可能抛出异常:

1
2
void recoup() noexcept(true);	/** 不会抛出异常 */
void alloc() noexcept(false); /** 可能抛出异常 */

noexcept说明符的实参常常与noexcept运算符混合使用。noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。和sizeof类似,noexcept也不会求其运算对象的值。

1
2
noexcept(recoup())	/** 如果recoup不跑出异常则结果为true;否则结果为false */
noexcept(e) /** 等价于上一句 */

我们可以使用noexcept运算符得到如下的异常说明:

1
void f() noexcept(noexcept(g()));	// f 和 g的异常说明一致

如果函数g()承诺了不会抛出异常,则f也不会抛出异常;如果g()没有异常说明符,或者g虽然有异常说明符但是允许抛出异常,则f()也可能抛出异常。

noexcept有两层含义:当跟在函数参数列表后面时它是异常说明符;而当作为noexcept异常说明的bool实参出现时,它是一个运算符。

4.3 异常说明与指针、虚函数和拷贝控制

函数指针及该指针所指的函数必须具有一致的异常说明。也就是说我们为某个指针做了不抛出异常的声明,则该指针将只能指向不抛出异常的函数。相反,如果我们显式或隐式地说明了指针可能抛出异常,则该指针可以指向任何函数,即使是承诺了不抛出异常的函数也可以。

​如果虚函数承诺了它不会抛出异常,则后续派生出来的虚函数也必须做出同样的承诺;与之相反如果基类的虚函数允许抛出异常,则派生类的对应函数既可以允许抛出异常,也可以不允许抛出异常。

​当编译器合成拷贝控制成员时,同时也生成一个异常说明。如果对所有成员基类的所有操作都承诺了不会抛出异常,则合成的成员是noexcept的。如果合成成员调用的任意一个函数可能抛出异常,则合成的成员是noexcept(false)。而且如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个。合成的异常说明将于假设有编译器为类合成析构函数时所得的异常说明一致。