0%

正确写法

该写法在第一次调用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)。而且如果我们定义了一个析构函数但是没有为它提供异常说明,则编译器将合成一个。合成的异常说明将于假设有编译器为类合成析构函数时所得的异常说明一致。

三路比较符(C++20)

官网解释

微软技术博客介绍

When do you actually use <=>?

例子1:

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
#include <iostream>
#include <compare>

struct Rational_2 {
int num;
int den; // > 0
};

constexpr std::weak_ordering operator<=>(Rational_2 lhs, Rational_2 rhs)
{
return lhs.num * rhs.den <=> rhs.num * lhs.den;
}

void print(std::weak_ordering value)
{
if (value == 0)
std::cout << "equal\n";
else if (value < 0)
std::cout << "less\n";
else
std::cout << "greater\n";
}

int main()
{
Rational_2 c{6,5};
Rational_2 d{8,7};
print(c <=> d);
print(std::compare_three_way{}(c,d));
}

例子2:

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
#include <iostream>
#include <compare>
struct Basics {
int i;
char c;
float f;
double d;
auto operator<=>(const Basics&) const = default;
};

struct Arrays {
int ai[1];
char ac[2];
float af[3];
double ad[2][2];
auto operator<=>(const Arrays&) const = default;
};

struct Bases : Basics, Arrays {
auto operator<=>(const Bases&) const = default;
};

int main() {
constexpr Bases a = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
constexpr Bases b = { { 0, 'c', 1.f, 1. },
{ { 1 }, { 'a', 'b' }, { 1.f, 2.f, 3.f }, { { 1., 2. }, { 3., 4. } } } };
static_assert(a == b);
static_assert(!(a != b));
static_assert(!(a < b));
static_assert(a <= b);
static_assert(!(a > b));
static_assert(a >= b);
}

严格弱序(strict weak ordering)

关联式容器(setmultisetmapmultimap)的排序准则的定义,和std::sort的排序准则定义必须遵守严格弱序,详细描述见官方解释(strict weak ordering.pdf)。

严格弱序的定义

简单的来说就是a<b返回true,a=b和a>b返回false。

详细定义:

  1. 必须是非对称的(antisymmetric)。

    operator< 而言, 如果x < y为true, 则y < x为false。

    对判断式(predicate) op()而言,如果op(x, y)为true,则op(y, x)为false。

  2. 必须是可传递的(transitive)。

operator< 而言,如果x < y 为true且y < z为true, 则x < z 为false。

对判断式(predicate) op()而言,如果op(x, y)为true且op(y, z)为tru,则op(x, z)为true。

  1. 必须是非自反的(irreflexive)

    operator< 而言,x < x 永远是false

    对判断式(predicate) op()而言,op(x, x)永远是false。

  2. 必须有等效传递性(transitivity of equivalence)

operator< 而言,假如 !(a<b) && !(b<a) 为true且 !(b<c) && !(c<b) 为 true
那么!(a<c) && !(c<a) 也为true.
对判断式(predicate) op()而言, 假如 op(a,b), op(b,a), op(b,c), 和op(c,b) 都为
false, 那么op(a,c) and op(c,a) 也为false.

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
// 一个定义std::set<struct>的例子
#include <set>
#include <iostream>

struct ORDERING_EXAMPLE
{
int x;
int y;
int z;

/// 重载遵循严格弱序的运算符<
bool operator < (const ORDERING_EXAMPLE& OtherStruct) const
{
if (this->x < OtherStruct.x)
return true;
if (OtherStruct.x < this->x)
return false;

// x == x则比较y
if (this->y < OtherStruct.y)
return true;
if (OtherStruct.y < this->y)
return false;

// y == y则比较z
if (this->z < OtherStruct.z)
return true;

return false;
}
};

int main()
{
std::set<ORDERING_EXAMPLE> setOrderingExample;

ORDERING_EXAMPLE stOrderingExample0 = { 0, 0, 0 };
ORDERING_EXAMPLE stOrderingExample1 = { 0, 1, 2 };
ORDERING_EXAMPLE stOrderingExample2 = { 0, 1, 3 };
ORDERING_EXAMPLE stOrderingExample3 = { 0, 1, 3 };

setOrderingExample.insert(stOrderingExample0);
setOrderingExample.insert(stOrderingExample1);
setOrderingExample.insert(stOrderingExample2);
setOrderingExample.insert(stOrderingExample3);

return 0;
}

下面举一个会崩溃的例子对二维数组排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <algorithm>
#include <iostream>
#include <vector>

int main() {
std::vector<int> temp(5, 1);
std::vector<std::vector<int>> vec(5, temp);
std::sort(vec.begin(), vec.end(),
[](const std::vector<int> &l, const std::vector<int> &r) {
if (l.size() == r.size()) {
for (size_t i = 0; i < l.size(); ++i) {
if (l.at(i) == r.at(i)) {
continue;
} else {
return l.at(i) < r.at(i);
}
}
return true; /// 这里会崩溃,改为false则不会而不会崩溃(遵循严格弱序)
} else {
return l.size() < r.size();
}
});
}

两个参数的重载符号简单示例

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
class key
{
public:
int x;
int y;

bool operator<(const key& stOther)
{
if (x < stOther.x)
{
return true;
}
else if (x > stOther.x)
{
return false;
}
else if (y < stOther.y)
{
return true;
}
else if (y > stOther.y)
{
return false;
}
else
{
return false;
}
}
};
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
class key
{
public:
int x;
std::string y;

bool operator<(const key& stOther)
{
if (x < stOther.x)
{
return true;
}
else if (x > stOther.x)
{
return false;
}
else if (y < stOther.y)
{
return true;
}
else if (y > stOther.y)
{
return false;
}
else
{
return false;
}
}
};