通用线程池

通用线程池和简单线程池的区别就是,简单线程池对交给线程的任务函数入参和返回值有类型要求,而通用的没有。在本文中,将讲述和简单线程池提交函数的区别。本文需要一定的 C++ 模板泛型基础。

和简单线程池的区别

简单线程池

在简单线程池中,增加任务给线程池的函数如下:

1
2
3
4
5
6
typedef std::function<void()> task_t;
void thread_pool::add_task(const task_t& task) {
std::unique_lock<std::mutex> lock(mutex_);
tasks_.push(task);
wake_cond_.notify_one(); // 只唤醒一个线程
}

可见,task_t 就是我们能向线程池提交的函数类型。它是一个 function 类型,返回值为 void,并且没有入参。那如果我们想提交一个返回值为 int,也有 int 作为入参的函数这样就行不通了。

通用线程池

于是我们将它略微改进下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 向线程池加入任务
template<class F, class... Args>
auto add_task(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
using return_type = decltype(f(args...));
auto task_ptr = std::make_shared<std::packaged_task<return_type()>>( // 因为 packaged_task 无法拷贝构造,所以用 make_shared
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);

std::future<return_type> res = task_ptr->get_future();
{
std::unique_lock<std::mutex> lock(mutex_);
tasks_.emplace([task_ptr]() {
(*task_ptr)();
});
}
wake_cond_.notify_one();
return res;
}

这个函数有两个模板参数,第一个模板参数 F 代表的是一个函数,第二个模板参数 Args 代表的是这个函数执行时的参数。该函数的返回值是一个 std::future 对象,可以通过 future.get() 来获得执行函数的返回值。具体用法可以见 future用法

因为不知道函数返回值是什么类型,所以需要用到 decltype,在编译时自动确定调用函数的返回值类型。届时从 future 对象中 get 到的数据类型就可以拿到了。在本函数中,先定义了一个指向 packaged_task 的智能指针(packaged_task 本质是个仿函数),之后把这个仿函数包装一层,让它变为一个返回值为 return_type,并且没有入参的函数。之所以用指针,是因为 packaged_task 无法进行拷贝构造,所以下面没法调用。之后在放入执行线程队列前,用 lambda 表达式封装它,让它变为返回值为 void 的函数,把它放到执行线程的队列中。最后该函数返回 packaged_task 对应的 future 对象,方便调用者拿到返回值。

值得注意的是,该函数因为是有模板参数,所以这个实现应该放在头文件中。原因详见:cpp模板类定义放在头文件原因

完整代码

以下代码是对 cpp11简单线程池 基础上做的改进:

main.cc

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
#include <iostream>
#include <chrono>
#include <future>
#include "thread_pool.h"

std::mutex g_screen_mutex; // 向终端打印信息的锁

int test(int a, int b) {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::lock_guard<std::mutex> lock(g_screen_mutex);
std::cout << "test() at thread [ " << std::this_thread::get_id() << "] output [" << a + b << "]" << std::endl;
return a + b;
}

int main() {
thread_pool thread_pool;
std::vector<std::future<int>> results;
for(int i = 0; i < 5 ; i++) {
auto fu = thread_pool.add_task(test, i, 2);
results.emplace_back(std::move(fu));
}
getchar(); // 等待,不要让主进程退出
for (int i = 0; i < 5; i++) {
std::cout<< "result of number " << i << " :" << results[i].get() << std::endl;
}
return 0;
}

thread_pool.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#ifndef _THREAD_POOL_H_
#define _THREAD_POOL_H_

#include <vector>
#include <queue>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>

class thread_pool {
public:
// 定义为一个函数类型,返回值为 void,没有入参
typedef std::function<void()> task_t;

thread_pool(int init_size = 3);
~thread_pool();
// 停止线程池
void stop();
// 向线程池加入任务
template<class F, class... Args>
auto add_task(F&& f, Args&&... args) -> std::future<decltype(f(args...))> {
using return_type = decltype(f(args...));
auto task_ptr = std::make_shared<std::packaged_task<return_type()>>(
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
);
std::future<return_type> res = task_ptr->get_future();
{
std::unique_lock<std::mutex> lock(mutex_);
tasks_.emplace([task_ptr]() {
(*task_ptr)();
});
}
wake_cond_.notify_one();
return res;
}
private:
thread_pool(const thread_pool&) = delete; // 禁止复制拷贝
const thread_pool& operator=(const thread_pool&) = delete;
// 线程池启动函数
void start();
// 每个线程的循环函数
void thread_loop();
// 从线程池里拿一个线程
task_t take();

int init_threads_size_; // 初始线程数量
std::vector<std::thread*> threads_; // 已经创建的线程列表
std::queue<task_t> tasks_; // 待执行任务列表
std::mutex mutex_; // 操作线程池共有变量之前先上锁
std::condition_variable wake_cond_; // 唤醒线程的条件
bool is_started_; // 线程池是否已经启动
};
#endif

thread_pool.cc:

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
#include <assert.h>
#include <iostream>
#include <future>
#include "thread_pool.h"

thread_pool::thread_pool(int init_size)
: init_threads_size_(init_size), mutex_(), wake_cond_(), is_started_(false) {
start();
}

thread_pool::~thread_pool() {
if (is_started_) {
stop();
}
}

void thread_pool::start() {
assert(threads_.empty());
is_started_ = true;
threads_.reserve(init_threads_size_);
for (int i = 0; i < init_threads_size_; ++i) {
threads_.push_back(new std::thread(std::bind(&thread_pool::thread_loop, this)));
}
}

void thread_pool::stop() {
std::cout << "thread_pool::stop() stop." << std::endl;
{
std::unique_lock<std::mutex> lock(mutex_);
is_started_ = false;
wake_cond_.notify_all();
std::cout << "thread_pool::stop() notifyAll()." << std::endl;
}

for (auto thread : threads_) {
thread->join();
delete thread;
}
threads_.clear();
}

void thread_pool::thread_loop() {
std::cout << "thread_pool::threadLoop() tid : " << std::this_thread::get_id() << " start." << std::endl;
while (is_started_) {
task_t task = take();
if (task) {
task();
}
}
std::cout << "thread_pool::threadLoop() tid : " << std::this_thread::get_id() << " exit." << std::endl;
}

thread_pool::task_t thread_pool::take() {
std::unique_lock<std::mutex> lock(mutex_);
// 使用 while 循环,防止假唤醒
while (tasks_.empty() && is_started_) {
std::cout << "thread_pool::take() tid : " << std::this_thread::get_id() << " wait." << std::endl;
wake_cond_.wait(lock);
}

std::cout << "thread_pool::take() tid : " << std::this_thread::get_id() << " wakeup." << std::endl;
task_t task;
size_t size = tasks_.size();
if (!tasks_.empty() && is_started_) {
task = tasks_.front();
tasks_.pop();
assert(size - 1 == tasks_.size());
}
if (task != nullptr) {
std::cout << "thread_pool::take() tid : " << std::this_thread::get_id() << " took a task!" << std::endl;
}
return task;
}

运行结果:

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
@└────> # ./test.out 
thread_pool::threadLoop() tid : 139812637472512 start.
thread_pool::take() tid : 139812637472512 wakeup.
thread_pool::take() tid : 139812637472512 took a task!
thread_pool::threadLoop() tid : 139812629079808 start.
thread_pool::take() tid : 139812629079808 wakeup.
thread_pool::take() tid : 139812629079808 took a task!
thread_pool::threadLoop() tid : 139812620687104 start.
thread_pool::take() tid : 139812620687104 wakeup.
thread_pool::take() tid : 139812620687104 took a task!
test() at thread [ 139812637472512] output [2]
thread_pool::take() tid : 139812637472512 wakeup.
thread_pool::take() tid : 139812637472512 took a task!
test() at thread [ 139812629079808] output [3]
thread_pool::take() tid : 139812629079808 wakeup.
thread_pool::take() tid : 139812629079808 took a task!
test() at thread [ 139812620687104] output [4]
thread_pool::take() tid : 139812620687104 wait.
test() at thread [ 139812637472512] output [5]
thread_pool::take() tid : 139812637472512 wait.
test() at thread [ 139812629079808] output [6]
thread_pool::take() tid : 139812629079808 wait.
(键入回车)
result of number 0 :2
result of number 1 :3
result of number 2 :4
result of number 3 :5
result of number 4 :6
thread_pool::stop() stop.
thread_pool::stop() notifyAll().
thread_pool::take() tid : 139812620687104 wakeup.
thread_pool::threadLoop() tid : 139812620687104 exit.
thread_pool::take() tid : 139812637472512 wakeup.
thread_pool::threadLoop() tid : 139812637472512 exit.
thread_pool::take() tid : 139812629079808 wakeup.
thread_pool::threadLoop() tid : 139812629079808 exit.

线程池

为什么需要线程池?因为频繁的创建和销毁线程,都涉及系统调用。频繁的系统调用涉及到用户态和内核态之间的切换,开销大。线程池的作用就是先把线程创建好,有活干就让线程去干活,没活线程就阻塞着。这样不需要频繁创建销毁线程,提升效率。除此之外,还可以防止过分调度。

图解

  1. 空闲时

1

  1. 有空闲线程时来任务,将任务加入到任务队列中,并唤醒一个线程。唤醒后线程拿到任务,把任务从任务队列中移出,并执行。

2

  1. 在没有空闲线程时来任务,任务排队。

3

代码

main.cc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <chrono>
#include "thread_pool.h"

std::mutex g_screen_mutex; // 向终端打印信息的锁

void testFunc() {
std::this_thread::sleep_for(std::chrono::seconds(1));
std::lock_guard<std::mutex> lock(g_screen_mutex);
std::cout << "===============================================================" << std::endl;
std::cout << "| testFunc() at thread [ " << std::this_thread::get_id() << "] output [" << 0 << "] |" << std::endl;
std::cout << "===============================================================" << std::endl;
}

int main() {
thread_pool thread_pool;
for(int i = 0; i < 5 ; i++) { // 往里面插入 5 个任务
thread_pool.add_task(testFunc);
}
getchar(); // 等待,不要让主进程退出
return 0;
}

thread_pool.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
34
35
36
37
38
39
40
#ifndef _THREAD_POOL_H_
#define _THREAD_POOL_H_

#include <vector>
#include <queue>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>

class thread_pool {
public:
// 定义为一个函数类型,返回值为 void,没有入参
typedef std::function<void()> task_t;

thread_pool(int init_size = 3);
~thread_pool();
// 停止线程池
void stop();
// 向线程池加入任务
void add_task(const task_t&);

private:
thread_pool(const thread_pool&) = delete; // 禁止复制拷贝
const thread_pool& operator=(const thread_pool&) = delete;
// 线程池启动函数
void start();
// 每个线程的循环函数
void thread_loop();
// 从线程池里拿一个线程
task_t take();

int init_threads_size_; // 初始线程数量
std::vector<std::thread*> threads_; // 已经创建的线程列表
std::queue<task_t> tasks_; // 待执行任务列表
std::mutex mutex_; // 操作线程池共有变量之前先上锁
std::condition_variable wake_cond_; // 唤醒线程的条件
bool is_started_; // 线程池是否已经启动
};
#endif

thread_pool.cc:

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
#include <assert.h>
#include <iostream>
#include "thread_pool.h"

thread_pool::thread_pool(int init_size)
: init_threads_size_(init_size), mutex_(), wake_cond_(), is_started_(false) {
start();
}

thread_pool::~thread_pool() {
if (is_started_) {
stop();
}
}

void thread_pool::start() {
assert(threads_.empty());
is_started_ = true;
threads_.reserve(init_threads_size_);
for (int i = 0; i < init_threads_size_; ++i) {
// 非静态成员函数则需要传递 this 指针作为第一个参数
threads_.push_back(new std::thread(std::bind(&thread_pool::thread_loop, this)));
}
}

void thread_pool::stop() {
std::cout << "thread_pool::stop() stop." << std::endl;
{
std::unique_lock<std::mutex> lock(mutex_); // 只有 unique_lock 才能和 condition_variable 配合使用
is_started_ = false;
wake_cond_.notify_all(); // 销毁前唤醒所有线程
std::cout << "thread_pool::stop() notifyAll()." << std::endl;
}

for (auto thread : threads_) {
thread->join(); // 销毁之前需要等待所有线程完成
delete thread;
}
threads_.clear();
}

void thread_pool::thread_loop() {
std::cout << "thread_pool::threadLoop() tid : " << std::this_thread::get_id() << " start." << std::endl;
while (is_started_) {
task_t task = take(); // 如果没有拿到会阻塞在这里
if (task) {
task();
}
}
std::cout << "thread_pool::threadLoop() tid : " << std::this_thread::get_id() << " exit." << std::endl;
}

void thread_pool::add_task(const task_t& task) {
std::unique_lock<std::mutex> lock(mutex_);
tasks_.push(task);
wake_cond_.notify_one(); // 只唤醒一个线程
}

thread_pool::task_t thread_pool::take() {
std::unique_lock<std::mutex> lock(mutex_);
// 使用 while 循环,防止假唤醒
while (tasks_.empty() && is_started_) {
std::cout << "thread_pool::take() tid : " << std::this_thread::get_id() << " wait." << std::endl;
wake_cond_.wait(lock); // 如果没任务的话线程会阻塞在这里
}

std::cout << "thread_pool::take() tid : " << std::this_thread::get_id() << " wakeup." << std::endl;
task_t task;
size_t size = tasks_.size();
if (!tasks_.empty() && is_started_) {
task = tasks_.front(); // 拿出队列中第一个任务
tasks_.pop();
assert(size - 1 == tasks_.size());
}
if (task != nullptr) {
std::cout << "thread_pool::take() tid : " << std::this_thread::get_id() << " took a task!" << std::endl;
}
return task;
}

运行结果:

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
@└────> # g++ main.cc thread_pool.cc -lpthread

@└────> # ./a.out
thread_pool::threadLoop() tid : 139841310938880 start.
thread_pool::take() tid : 139841310938880 wakeup.
thread_pool::take() tid : 139841310938880 took a task!
thread_pool::threadLoop() tid : 139841302546176 start.
thread_pool::take() tid : 139841302546176 wakeup.
thread_pool::take() tid : 139841302546176 took a task!
thread_pool::threadLoop() tid : 139841294153472 start.
thread_pool::take() tid : 139841294153472 wakeup.
thread_pool::take() tid : 139841294153472 took a task!
===============================================================
| testFunc() at thread [ 139841310938880] output [0] |
===============================================================
thread_pool::take() tid : 139841310938880 wakeup.
thread_pool::take() tid : 139841310938880 took a task!
===============================================================
| testFunc() at thread [ 139841302546176] output [0] |
===============================================================
thread_pool::take() tid : 139841302546176 wakeup.
thread_pool::take() tid : 139841302546176 took a task!
===============================================================
| testFunc() at thread [ 139841294153472] output [0] |
===============================================================
thread_pool::take() tid : 139841294153472 wait.
===============================================================
| testFunc() at thread [ 139841310938880] output [0] |
===============================================================
thread_pool::take() tid : 139841310938880 wait.
===============================================================
| testFunc() at thread [ 139841302546176] output [0] |
===============================================================
thread_pool::take() tid : 139841302546176 wait.
(键入回车)
thread_pool::stop() stop.
thread_pool::stop() notifyAll().
thread_pool::take() tid : 139841294153472 wakeup.
thread_pool::threadLoop() tid : 139841294153472 exit.
thread_pool::take() tid : 139841310938880 wakeup.
thread_pool::threadLoop() tid : 139841310938880 exit.
thread_pool::take() tid : 139841302546176 wakeup.
thread_pool::threadLoop() tid : 139841302546176 exit.

从结果可以看出,确实是三个线程在执行 5 个任务,并且线程执行完成后并没有销毁。这里的函数都是 void() 类型,即返回值为 void,并且没有参数,不具备泛用性。如果需要线程执行其他类型函数则需要包装函数类。

通用线程池请见:通用线程池

编译单元

编译单元是指一个源文件(.c/.cpp)文件和包含展开的头文件,之后编译器会将该文件编译成一个 object 文件(在 linux 中是 .o 文件)。

1
gcc xxx.cc -c

之后用可以看见当前目录下生成一个 xxx.o 的文件,这个文件就是 xxx.cc 编译单元生成的对应二进制。这个二进制里面有该文件所对应的符号。

普通成员函数

现在有如下例子,尝试编译对应的二进制文件,不链接:

main.cc

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
a.print();
}

A.h:

1
2
3
4
5
class A {
public:
int m;
void print();
};

A.cc

1
2
3
4
5
6
7
#include <iostream>
#include "A.h"
using namespace std;

void A::print() {
cout << m << endl;
}

之后可以用 nm 命令查看二进制中的符号,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@└────> # g++ main.cc -c
@└────> # g++ A.cc -c
@└────> # nm main.o
0000000000000000 T main
U _ZN1A5printEv
@└────> # nm A.o
U __cxa_atexit
U __dso_handle
000000000000006c t _GLOBAL__sub_I__ZN1A5printEv
000000000000002e t _Z41__static_initialization_and_destruction_0ii
0000000000000000 T _ZN1A5printEv
U _ZNSolsEi
U _ZNSolsEPFRSoS_E
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _ZSt4cout
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

可以看到,在 main.o 中,有一个 _ZN1A5printEv,是对 undefined,也就是说编译器在这个编译单元中找不到这个符号。在 main.o 中,看下反汇编,调用 print 函数的时候是类似 call print 这种形式跳转函数的。因为找不到这个符号,自然也就不知道这个函数的地址,所以需要链接器链接有这个符号的 A.o,就可以运行了。常规的成员函数这样运行的,那么如果换成模板成员函数会怎么样呢?

模板成员函数

修改代码如下:

main.cc

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
a.print<int>();
}

A.h:

1
2
3
4
5
6
class A {
public:
int m;
template<class T>
void print();
};

A.cc

1
2
3
4
5
6
7
#include <iostream>
#include "A.h"
using namespace std;
template<class T>
void A::print() {
cout << m << endl;
}

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@└────> # g++ main.cc -c
@└────> # g++ A.cc -c
@└────> # nm main.o
0000000000000000 T main
U _ZN1A5printIiEEvv
@└────> # nm A.o
U __cxa_atexit
U __dso_handle
000000000000003e t _GLOBAL__sub_I_A.cc
0000000000000000 t _Z41__static_initialization_and_destruction_0ii
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

可以看到,在换成模板函数后,A.o 中根本没有 print 的符号!尽管 A.cc 中有定义。这是因为在 C++ 中,当一个模板不被用到的时侯它就不该被实例化出来,而在 A.cc 中显然这个模板没有被用到。所以编译器在编译 main.o 的时候,没找到符号,就把希望寄托于链接器。而链接器在链接的时候也懵逼了,因为这个符号在 A.o 中也没有。所以这种情况下链接器会报找不到符号的错误。

解决方案

所以回到主题,这就是为什么对于 C++ 的模板函数来说,要把模板函数的实现放到头文件中。

main.cc

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
a.print<int>();
}

A.h:

1
2
3
4
5
6
7
8
9
#include <iostream>
class A {
public:
int m;
template<class T>
void print() {
std::cout << m << std::endl;
}
};

编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@└────> # g++ main.cc -c
@└────> # nm main.o
U __cxa_atexit
U __dso_handle
0000000000000059 t _GLOBAL__sub_I_main
0000000000000000 T main
000000000000001b t _Z41__static_initialization_and_destruction_0ii
0000000000000000 W _ZN1A5printIiEEvv
U _ZNSolsEi
U _ZNSolsEPFRSoS_E
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _ZSt4cout
U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit

可以看到 main.o 中包含 print 的符号。

auto

C++11 中,使用 auto 关键字来进行自动类型推导。语法如下:

1
auto name = value;

其中,name 是变量的名字。value 是变量的初始值。其中 auto 是一个占位符,会在编译期间由编译器推导出来。而且由 auto 推导的变量必须初始化,因为是占位符的原因,不能用作声明。

限制:

  1. auto 不能在函数的参数中使用。

我们在定义函数的时候只是对参数进行了声明,指明了参数的类型,但并没有给它赋值,只有在实际调用函数的时候才会给参数赋值。而 auto 要求必须对变量进行初始化。

  1. auto 不能作用于类的非静态成员变量(即没有 static 关键字修饰的成员变量)中。
  2. auto 关键字不能定义数组,比如下面的例子就是错误的:
1
2
char a[] = "abcdefg";  
auto b[] = a; // a 为数组,所以不能使用 auto
1
error: ‘b’ declared as array of ‘auto’
  1. auto 不能作用于模板参数
1
2
3
4
5
6
7
8
9
template <typename T>
class A {
//TODO:
};

int main(){
A<int> C1;
A<auto> C2 = C1; // 错误
}
1
error: invalid use of ‘auto’

decltype

decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。
auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

1
2
3
auto varname = value;
decltype(exp) varname = value;
decltype(exp) varname;

其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。

auto 根据等号右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟等号右边的 value 没有关系。

另外,auto 要求变量必须初始化,而 decltype 不要求。auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。此外,我们必须要保证 exp 的结果是有类型的,不能是 void。例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。

1
error: variable or field ‘b’ declared void

注意:

  1. 如果 exp 是一个不被括号包围的表达式,或者是类成员访问表达式,或者是单独的变量,那么 decltype(exp) 的类型就和 exp 一致。
  2. 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致。
  3. 如果 exp 是一个左值,或者被括号包围,那么 decltype(exp) 的类型就是 exp 的引用。假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。
  4. decltype 如果是函数调用,因为是在编译时确定,和 sizeof 一样,不会调用一次函数。

多态

  1. 静态多态:相同对象接收到不同的消息,产生不同的结果 => 函数重载。
  2. 动态多态:不同对象接收到相同的消息,产生不同的结果 => 虚函数。

多态的作用就是为了提供高度统一的接口,实现代码复用,提高代码的可扩展性和可维护性。

虚函数

虚函数表

虚函数的实现原理可见另一文章:关于cpp虚函数表的实现原理

使用限制

0. virtual 具有继承性: 父类中定义为 virtual 的函数在子类中重写的函数也自动成为虚函数。需要注意的是,只有子类的虚函数和父类的虚函数定义完全一样才被认为是虚函数。

1. virtual 不能修饰类外的普通函数,只能修饰类中的成员函数(普通函数和析构函数)。

这句话很好理解,就是 virtual 关键字不能修饰全局函数。

1
2
3
virtual int test () { // 不正确,不能修饰全局函数
// do some thing
}

编译报错:

1
error: ‘virtual’ outside class declaration

2. virtual 不能修饰构造函数。

  • 从存储空间角度,虚函数的每个对象内部都有一个指向虚函数表的指针。那么虚函数表是什么时候初始化的呢?就是在调用构造函数的时候。cpp 调用 new 的步骤是,先申请所需的内存(malloc/operator new),再调用构造函数。如果构造函数是虚的,那必须通过虚函数表指针来找到虚构造函数的入口地址。然而此时申请的内存还没有初始化,不可能由虚函数表和虚函数表指针的。综上所述,构造函数不能为虚函数。
  • 从使用角度,构造函数是创建对象时调用的,不可能通过父类的指针或引用去调用。创建一个对象时,总是要明确指定对象的类型。但是析构函数不一样,可以通过基类指针进行析构。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

class A {
public:
virtual A() {
cout << "construct" << endl;
}
};

int main() {
A* a = new A();
}

编译报错:

1
error: constructors cannot be declared ‘virtual’

3. virtual 不能修饰静态成员函数。

虚函数的调用时经过对象内部的虚函数表指针,找到虚函数表,进而去调用对应的函数。但是静态成员函数和实例没有关系,只和类有关。所以调用时不会隐式传入 this 指针。题外话,因为没有 this 指针,静态函数也不能声明为 const 和 volatile。因为 void func() const {} 会被转换为 void func(const A *this) {};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "construct" << endl;
}
virtual static void print() {
cout << "print" << endl;
}
};

int main() {
A* a = new A();
a->print();
}

编译报错:

1
error: member ‘print’ cannot be declared both ‘virtual’ and ‘static’

4. virtual 不能修饰内联函数。

内联函数关键字 inline 是对编译器的一个建议:如果可能,请把此函数变为内联函数。虚函数是由虚函数表实现的,因为表中需要存放函数的地址。被内联展开的函数没有具体的地址,所以无法被 virtual 修饰。但是以下写法也不会报编译错误。

1
2
3
4
5
6
7
8
9
class A {
public:
A() {
cout << "construct" << endl;
}
virtual inline void print() {
cout << "print" << endl;
}
};

因为内联函数的建议编译器没有采纳,就和没有加 inline 关键字效果一样。

5. virtual 不能修饰友元函数。

友元函数是可以访问类内私有成员的非成员函数,是定义在类外的普通函数。它不属于某个类,所以自然无法被 virtual 修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "construct" << endl;
}
virtual friend void print();
};

void print() {
cout << "print" << endl;
}

int main() {
A* a = new A();
print();
}

编译报错:

1
error: virtual functions cannot be friends

vector 是 cpp 中常用的一种容器,是一种可以动态扩容的数组。他在每次插入的时候,当它的 capacity 不满足要求的时候,会重新申请一块两倍的内存,把之前的内容复制过来。在这种情况下,复制的时间复杂度为 O(n)O(n)。如果不涉及扩容,那么它只修改一个元素的内存,时间复杂度为 O(1)O(1)

也就是说,它扩容的时机为,容器大小达到以下时:

1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536 …

则每次插入代价设为 cic_i,则

ci={ii2的幂1othersc_i = \begin{cases} i & i 为 2 的幂\\ 1 & others \\ \end{cases}

那么则有:

i=0ncin+j=0log2n2jn+2n3n\begin{aligned} \sum_{i = 0}^nc_i &\le n + \sum_{j = 0}^{\lfloor log_2n \rfloor}2^j \\ &\le n + 2n\\ &\le 3n \end{aligned}

因为共有 nn 次插入,所以总时间复杂度为 O(3n)O(3n),均摊下来平均时间复杂度为 O(3)O(3)

单例模式概念

定义

单例模式 Singleton:该类只能实例化一个对象的设计模式。

分类

  1. 懒汉模式:延时加载,在这个唯一的对象使用时才加载。

    优点

    1. 资源利用率高,使用时才创建,不存在浪费。
    2. 加载类速度快,不需要创捷对象。

    缺点

    1. 存在线程安全问题,需要引入同步机制(锁、信号量)。创建唯一对象不是原子操作,判空也不是原子操作。
    2. 运行获取速度慢,多线程同步带来额外开销。
  2. 饿汉模式:贪婪加载,在类加载时就生成唯一的对象。

    优点

    1. 线程安全的单例模式。因为在加载的时候就创建了,所以不会有竞争。
    2. 获取对象速度快,因为已经创建好了,只需要获取就可以。

    缺点

    1. 存在资源浪费的可能。因为可能只是加载这个类却不使用他。
    2. 类加载速度慢,因为需要创建对象。

设计思想

如何控制,达到只生成一个对象的目的。

  1. 对生成对象进行控制,将构造函数设计为私有。这样类外就不能调用构造函数,达到控制对象生成的目的。
  2. 对拷贝构造函数进行控制,将其设为私有并不实现,或者进行删除(C++11)。

提供对外接口,生成唯一对象。

这个接口需要判断是否只生成了一个对象。

  1. 这个函数的返回值需要返回一个对象,分三种情况。
    1. 返回对象类名,存在临时对象,会发生拷贝,不符合单例模式要求。
    2. 返回对象引用,没有拷贝发生。
    3. 返回对象的指针,没有拷贝发生。
  2. 修饰关键字设置,从调用角度考虑。
    1. 依赖对象来调用,obj.func() 来生成。但是此时对象未生成,所以不符合要求。
    2. 设计为静态函数,不依赖对象调用。通过类作用域来调用 ClassName::func() 来生成。
  3. 有一个标识,来标识唯一的对象。
    1. 为了防止这个接口被调用很多次,定义一个唯一标识对象的指针,这个指针不依赖于对象。所以还是定义为静态变量,类外初始化为 nullptr。
    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
#include <iostream>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
if (instance_ == nullptr) {
instance_ = new LazySingleton();
}
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
int main() {
LazySingleton *obj1 = LazySingleton::GetInstance();
LazySingleton *obj2 = LazySingleton::GetInstance();
LazySingleton *obj3 = LazySingleton::GetInstance();
cout << "obj1:" << obj1 << endl;
cout << "obj2:" << obj2 << endl;
cout << "obj3:" << obj3 << endl;
return 0;
}

输出:

1
2
3
4
5
@└────> # ./a.out 
create singleton object:0x1861eb0
obj1:0x1861eb0
obj2:0x1861eb0
obj3:0x1861eb0

这种懒汉模式的单例,在单线程下是没问题的,但是多线程下有竞争就会出现问题。比如两个线程都进行完了 instance_ 的判断,并且都为空,那么他们都会调用那个构造函数。

多线程版本

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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
m_.lock();
if (instance_ == nullptr) {
instance_ = new LazySingleton();
}
m_.unlock();
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
static mutex m_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
mutex LazySingleton::m_;
int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}

结果:

1
2
3
4
5
@└────> # ./a.out 
thread id:139862718146304 instance pointer:create singleton object:0x7f344c000f70
0x7f344c000f70
thread id:139862709753600 instance pointer:0x7f344c000f70
thread id:139862701360896 instance pointer:0x7f344c000f70

加锁,保证一个线程在对临界资源进行操作时不能被其他线程打断。用互斥锁即可,但是如果每次调用都需要进行加锁解锁,在第一个对象已经生成之后,在读的层面不需要进行加锁了。加锁解锁会导致效率低下。

可以把上面的加锁部分套在一个 if 判断里,这样下个线程进来的时候就不用试图加锁了。

改进版代码:

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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
if (instance_ == nullptr) { // 套起来
m_.lock();
if (instance_ == nullptr) {
instance_ = new LazySingleton();
}
m_.unlock();
}
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
static mutex m_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
mutex LazySingleton::m_;
int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}

结果和上面是一样的,这里就不展示了。

C++11 的多线程版本

在 C++11 中,引入了 std::call_once,可以更便捷的实现上述功能。如需使用,只需要 #include 即可,简单来说 std:call_once 的作用,确保函数或代码片段在多线程环境下,只需要执行一次,常用的场景如 Init() 操作或一些系统参数的获取等。

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
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;

class LazySingleton {
public:
static LazySingleton *GetInstance() {
std::call_once(instnace_flag_, []() {
instance_ = new LazySingleton();
});
return instance_;
}
private:
LazySingleton() {
cout << "create singleton object:" << this << endl;
}
LazySingleton(const LazySingleton& s) = delete;
static LazySingleton *instance_;
static once_flag instnace_flag_;
};
LazySingleton* LazySingleton::instance_ = nullptr;
once_flag LazySingleton::instnace_flag_;
int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << LazySingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}

结果:

1
2
3
4
5
@└────> # ./a.out 
thread id:140655525984000 instance pointer:create singleton object:0x7fece4000f70
0x7fece4000f70
thread id:140655517591296 instance pointer:0x7fece4000f70
thread id:140655509198592 instance pointer:0x7fece4000f70

饿汉模式代码

如果我们在线程开启前,已经将唯一的对象生成,就不会出现线程不安全的问题。即唯一的对象在main函数调用前已经生成,就不会产生线程安全问题。所以:

  1. 在未进入 main 函数时,就完成唯一对象的生成,那么就是在程序加载时已经完成,存放在 .data段 的数据有这样的特点,所以我们可以直接给静态成员变量生成对象。
  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
#include <iostream>
#include <thread>
using namespace std;

class HungerSingleton {
public:
static HungerSingleton *GetInstance() {
return instance_;
}
private:
HungerSingleton() {
cout << "create singleton object:" << this << endl;
}
HungerSingleton(const HungerSingleton& s) = delete;
static HungerSingleton *instance_;
};
HungerSingleton* HungerSingleton::instance_ = new HungerSingleton();

int main() {
thread t1([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << HungerSingleton::GetInstance() << endl;
});
thread t2([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << HungerSingleton::GetInstance() << endl;
});
thread t3([]() {
cout << "thread id:" << this_thread::get_id() << " instance pointer:" << HungerSingleton::GetInstance() << endl;
});
t1.join();
t2.join();
t3.join();
return 0;
}
1
2
3
4
5
@└────> # ./a.out 
create singleton object:0x2343eb0
thread id:140504409663232 instance pointer:0x2343eb0
thread id:140504401270528 instance pointer:0x2343eb0
thread id:140504392877824 instance pointer:0x2343eb0

什么是多路复用

操作系统在处理 I/O 的时候,分为两个阶段:

  1. 等待数据到达内核空间。
  2. 等待数据从内核空间拷贝到用户空间。

在 Linux 操作系统中,多路复用的方式有三种。分别是 select,poll,和 epoll。I/O 多路复用为,只通过一种机制,可以监视多个描述符,一旦某个描述符就绪(读就绪/写就绪),能够通知程序进行相应的读写操作。

但 select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

select

单个进程就可以同时处理多个网络连接的 I/O 请求(同时阻塞多个 I/O 操作)。

基本原理就是:

  1. 程序调用 select,然后整个程序就阻塞状态。
  2. 这时候,内核就会轮询检查所有 select 负责的文件描述符集合 fd_set(一般上限为 1024 个),当找到其中那个的数据准备好了的文件描述符返回给 select。
  3. select 进行系统调用,将数据从内核复制到进程缓冲区(用户空间)。

1

select的缺点:

  1. 每次调用 select,都需要把 fd_set 从用户态拷贝到内核态,这个开销在 fd 很多时会很大。
  2. 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
  3. select 支持的文件描述符数量太小了,默认是 1024

poll

poll的原理与select非常相似,差别如下:

  1. 描述fd集合的方式不同,poll 使用 pollfd 结构而不是 fd_set 结构,是链式的,所以没有最大连接数的限制。
  2. poll 有一个特点是水平触发,也就是通知程序 fd 就绪后,这次没有被处理,那么下次 poll 的时候会再次通知同样的 fd 已经就绪。

poll 解决了 fd_set 有上限的情况,并且每次读就绪描述符集合时不用保存之前的状态。

epoll

epoll 提供了三个函数:

1
2
3
4
5
6
7
8
int epoll_create(int size);
// 建立一個 epoll 对象,并传回它的id

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// 等待注册的事件被触发或者timeout发生

epoll解决的问题:

  1. epoll没有fd数量限制

epoll没有这个限制,我们知道每个 epoll 监听一个 fd,所以最大数量与能打开的 fd 数量有关,一个 G 的内存的机器上,能打开10万个左右

  1. epoll 不需要每次都从用户空间将 fd_set 复制到内核

epoll 在用 epoll_ctl 函数进行事件注册的时候,已经将 fd 复制到内核中,所以不需要每次都重新复制一次

  1. select 和 poll 都是主动轮询机制,需要遍历每一个人 fd;而 epoll 是被动触发方式。

epoll_ctl 给 fd 注册了相应事件的时候,我们为每一个 fd 指定了一个回调函数,当数据准备好之后,就会把就绪的 fd 加入一个就绪的队列中,epoll_wait 的工作方式实际上就是在这个就绪队列中查看有没有就绪的 fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。

虽然 epoll, poll, select 都需要查看是否有 fd 就绪,但是 epoll 之所以是被动触发,就在于它只要去查找就绪队列中有没有 fd,就绪的 fd 是主动加到队列中,epoll 不需要一个个轮询确认。

换一句话讲,就是 select 和 poll 只能通知有 fd 已经就绪了,但不能知道究竟是哪个 fd 就绪,所以 select 和 poll 就要去主动轮询一遍所有监听的 fd 才能找到就绪的 fd。而 epoll 则是不但可以知道有 fd 可以就绪,而且还具体可以知道就绪 fd 的编号,所以直接找到就可以,不用轮询。

注意,epoll 只有 Linux 上面有。

callgrind

callgrind 是 valgrind 套件中的一个工具,使用方式为

1
valgrind --tool=callgrind ./your_program your_program_args1 your_program_args2

运行之后,可以生成一份 callgrind.out.pid 的文件,在当前目录下。这份文件记录了运行时的一些数据,以及函数调用关系。

kcachegrind

kcachegrind 可以分析上述产生的 callgrind.out.pid 的文件,并且点击下方 call graph 可以看到调用关系图,帮助我们分析代码结构。

点击下载windows版kcachegrind

示例

代码:

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

int accumulate(int begin, int end) {
int result = 0;
for (int i = begin; i < end; i++) {
result += i;
}
}

int accumulate0_100000() {
return accumulate(0, 100000);
}

int accumulate0_200000() {
return accumulate(0, 200000);
}

int main() {
accumulate0_100000();
accumulate0_200000();
return 0;
}
1
valgrind --tool=callgrind --dump-instr=yes --trace-jump=yes ./a.out

kcachegrind分析

生成文件后,使用 kcachegrind 工具打开这个分析文件。

1

可以从图中看到函数执行时间,百分比等功能。

gprof2dot分析

使用 gprof2dot 工具,需要安装 python 和 graphviz。

1
2
yum install graphviz
pip3 install gprof2dot

之后使用上述工具对其进行分析,生成调用图。

1
2
gprof2dot -f callgrind -n0 -e0 ./callgrind.out.2328 > callgrind.2328.dot
dot callgrind.2328.dot -Tsvg -o callgrind.2328.dot.svg

其中 -f 指定输出格式为 callgrin d格式,-nX 指定生成的文件忽略小于 X 个 node 的函数,例如 -n10,代表函数节点小于 10 个的,在最终文件中不生成其相关信息,-eX 代表边缘阈值,与 -n 类似。

四种转换介绍

C风格转换:转换的含义是通过改变一个变量的类型为别的类型从而改变该变量的表示方式。为了类型转换一个简单对象为另一个对象,会使用传统的类型转换操作符。比如,为了转换一个类型为 double 的浮点数的指针到整型:

1
2
3
4
int i;
double d;
i = (int)d;
i = int(d);

对于具有标准定义转换的简单类型而言很好。但是这样的转换符不能随意用于类(class)和类的指针。标准定义了四个新的转换符:reinterpret_cast, static_cast, dynamic_cast 和 const_cast,目的在于控制类之间的类型转换。

reinterpret_cast

reinterpret_cast 转换一个指针为其它类型的指针。它也允许从一个指针转换为整数类型。反之亦然。这个操作符能够在非相关的类型之间转换。操作结果只是简单的从一个指针到别的指针的值的二进制拷贝。在类型之间指向的内容不做任何类型的检查和转换。

代码:

1
2
3
4
class A {};
class B {};
A *a = new A;
B *b = reinterpret_cast<B *>(a);

reinterpret_cast 就像传统的类型转换一样对待所有指针的类型转换。

static_cast

static_cast 允许执行任意的隐式转换和相反转换动作。(相关类之间转换)

应用到类的指针上,意思是说它允许子类类型的指针转换为父类类型的指针(这是一个有效的隐式转换),同时,也能够执行相反动作:转换父类为它的子类。

在这最后例子里,被转换的父类没有被检查是否与目的类型相一致。

代码:

1
2
3
4
class Base {};
class Derived : public Base {};
Base *a = new Base;
Derived *b = static_cast<Derived *>(a);

static_cast 除了操作类型指针,也能用于执行类型定义的显式的转换,以及基础类型之间的标准转换:

1
2
double d = 3.14159265;
int i = static_cast<int>(d);

如果转换的类型之间没有关系的话,会报编译错误:

1
2
3
4
5
@└────> # g++ test.cc 
test.cc: In function ‘int main()’:
test.cc:12:41: error: invalid static_cast from type ‘Base*’ to type ‘Derived*’
Derived *b = static_cast<Derived *>(a);
^

如果子类中有基类没有的对象,那么转换后使用则不安全。

dynamic_cast

dynamic_cast 只用于对象的指针和引用。当用于多态类型时,它允许任意的隐式类型转换以及相反过程。不过,与 static_cast 不同,在自上而下的转换过程中, dynamic_cast 会检查操作是否有效。也就是说,它会检查转换是否会返回一个被请求的有效的完整对象。
检测在运行时进行。如果被转换的指针不是一个被请求的有效完整的对象指针,返回值为NULL.
代码:

1
2
3
4
5
6
7
8
class Base { virtual dummy() {} };
class Derived : public Base {};

Base* b1 = new Derived;
Base* b2 = new Base;

Derived* d1 = dynamic_cast<Derived *>(b1); // 成功
Derived* d2 = dynamic_cast<Derived *>(b2); // 失败,返回NULL

用于安全向下转型,成本非常高昂。

const_cast

这个转换类型操纵传递对象的 const 属性,或者是设置或者是移除
代码:

1
2
3
class C {};
const C *a = new C;
C *b = const_cast<C *>(a);

其它三种操作符是不能修改一个对象的常量性的。
const_cast 在调用第三方函数中的使用:
在使用第三方库或API时,它们只提供了非 const 类型的参数的函数,但我们只有 const 类型的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;
int third_lib_fun(int *ptr) {
*ptr = *ptr + 10;
return (*ptr);
}
int main(void) {
int val = 10;
const int *ptr = &val;
int *ptr1 = const_cast<int *>(ptr);
third_lib_fun(ptr1);
cout << val;
return 0;
}

输出结果:

1
20

我们在使用第三方库和 API 的时候,我们只能调用,看不到其具体的实现,为了能够调用成功,需要使用 const_cast 来去除 *ptr 的 const 属性。