C++ 巩固进阶之三

大纲

C++ 11 的 function 类模板

概念介绍

function 是 C++ 11 引入的一个类模板,用于存储任何可以调用的目标(如普通函数、函数指针、函数对象、Lambda 表达式等),并通过统一的接口进行调用。它能够封装和管理函数,允许将函数作为对象传递和存储,位于 <functional> 头文件中,常用于回调和高阶函数。

  • 使用说明

    • 用函数类型实例化 function 类模板
    • 通过 function 类模板调用 operator() 函数的时候,需要根据函数类型传入相应的参数
  • 使用特点

    • 通过类型擦除技术存储任意可调用对象(普通函数、Lambda 表达式、仿函数、成员函数指针等),无需关心具体类型,仅需关注调用签名‌
    • 编译时会检查参数和返回类型是否匹配,避免了传统函数指针的类型不安全问题‌
  • 使用场景

    • 事件回调
    • 作为函数参数传递可调用对象
    • 存储 Lambda 表达式

对比说明

C 语言中的函数指针与 C++ 的 function 类模板的对比如下:

特性函数指针 (C 语言)function 类模板 (C++)
灵活性只能指向具有匹配签名的函数可以封装多种类型的可调用对象 (函数、Lambda、函数对象)
状态管理不支持状态封装支持状态封装(如 Lambda 表达式和函数对象)
类型安全不提供额外的类型安全提供类型安全检查
性能开销较低可能有较高的内存和性能开销
多态性不支持多态支持多态

C 语言中的函数指针与 C++ 的 function 类模板的区别如下:

  • 基本概念

    • 函数指针(C 语言)
      • 函数指针是一个变量,用于存储函数的地址。通过该指针,可以间接调用对应的函数。
      • C 语言中的函数指针需要显式指定函数签名(返回值类型和参数类型)。
    • function 类模板(C++):
      • function 是 C++ 11 引入的类模板,提供了一种通用的、类型安全的方式来存储、传递和调用可调用对象(如普通函数、函数指针、Lambda 表达式、函数对象等)。
      • 它可以封装多种不同类型的可调用对象,并且允许它们通过统一的接口被调用。
  • 灵活性和类型支持

    • 函数指针(C 语言):
      • 函数指针只能指向具有相同函数签名(参数类型和返回类型)的函数。
      • 不支持封装函数对象、Lambda 表达式等其他类型的可调用对象。
    • function 类模板(C++):
      • function 支持多种类型的可调用对象,包括普通函数、函数指针、Lambda 表达式、函数对象等。
      • 它的模板参数可以适配任意函数签名,支持更加灵活的调用。
      • 还支持捕获外部状态的 Lambda 表达式,或者包含状态的函数对象。
  • 状态管理

    • 函数指针(C 语言):
      • 函数指针无法封装额外的状态信息,指向的仅仅是一个函数。
      • 如果需要管理状态(如在函数调用前后执行某些操作),必须依赖外部的代码来实现。
    • function 类模板(C++):
      • function 可以封装状态信息。例如,Lambda 表达式和函数对象可以拥有成员变量和成员函数,允许在调用时使用封装的状态。
      • 这种能力使得 function 在处理回调和事件处理时更具优势。
  • 类型安全

    • 函数指针(C 语言):
      • 函数指针本身类型是静态的,编译时要求函数签名必须匹配,但它不提供额外的类型安全检查,错误的使用可能导致未定义行为。
    • function 类模板(C++):
      • function 是类型安全的,编译时会检查存储的可调用对象是否与声明的函数签名一致。
      • C++ 编译器提供了类型安全的保证,避免了函数签名不匹配带来的错误。
  • 内存管理和性能

    • 函数指针(C 语言):
      • 函数指针直接存储函数的地址,不涉及额外的内存分配,因此它的性能开销较小。
      • 由于没有额外的封装,也没有多态或状态的管理,性能上较为高效。
    • function 类模板(C++):
      • function 需要进行额外的内存分配和类型擦除(Type Erasure)。对于 Lambda 表达式和函数对象,它会封装一个通用的接口,这可能带来额外的性能开销。
      • 但是,它的灵活性和类型安全是 function 的优势。
  • 多态和扩展性

    • 函数指针(C 语言):
      • 函数指针没有多态性。它们只是简单地指向某个函数,无法支持运行时多态。
    • function 类模板(C++):
      • function 支持通过函数对象、Lambda 表达式和虚拟函数等方式实现运行时多态,提供了更大的灵活性。
      • 它可以封装复杂的行为,例如结合面向对象编程中的继承和多态来实现不同的回调机制。

使用案例

案例代码一

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
#include <iostream>
#include <functional>
#include <string>

using namespace std;

void hello1() {
cout << "hello world" << endl;
}

void hello2(string str) {
cout << "hello " << str << endl;
}

int sum(int a, int b) {
return a + b;
}

int main() {
// function 函数对象类型
function<void()> function1 = hello1;
function1();

function<void(string)> function2 = hello2;
function2("peter");

function<int(int, int)> function3 = sum;
int total = function3(1, 2);
cout << "total = " << total << endl;

// function 函数对象类型 + Lambda 表达式
function<int(int, int)> function4 = [](int a, int b) {return a + b; };
int total2 = function4(3, 5);
cout << "total2 = " << total2 << endl;

return 0;
}

程序运行输出的结果如下:

1
2
3
4
hello world
hello peter
total = 3
total2 = 8

案例代码二

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
#include <iostream>
#include <functional>
#include <string>

using namespace std;

class Test {

public:

void hello(string str) {
cout << "hello " << str << endl;
}

};

int main() {
// 通过 function 函数对象类型,调用类的成员函数
Test t;
function<void(Test*, string)> function1 = &Test::hello;
function1(&t, "peter");
function1(&Test(), "peter"); // 或者使用临时变量

return 0;
}

程序运行输出的结果如下:

1
2
hello peter
hello peter

案例代码三

使用 function 函数对象类型来实现图书管理系统的菜单列表选择功能。

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
#include <iostream>
#include <functional>
#include <string>
#include <map>

using namespace std;

void doShowAllBooks() {
cout << "查看所有书籍" << endl;
};

void doBorrowBook() {
cout << "借书" << endl;
};

void doBackBook() {
cout << "还书" << endl;
}

void doQueryBook() {
cout << "查询书籍" << endl;
}

void doLoginOut() {
cout << "注销" << endl;
}

int main() {
int choice = 0;

map<int, function<void()>> actionMap;
actionMap.insert({ 1, doShowAllBooks });
actionMap.insert({ 2, doBorrowBook });
actionMap.insert({ 3, doBackBook });
actionMap.insert({ 4, doQueryBook });
actionMap.insert({ 5, doLoginOut });

for (;;) {
cout << "\n-------------------" << endl;
cout << "1. 查看所有书籍" << endl;
cout << "2. 借书" << endl;
cout << "3. 还书" << endl;
cout << "4. 查询书籍" << endl;
cout << "5. 注销" << endl;
cout << "-------------------" << endl;
cout << "请选择: ";

// 检测输入是否合法
if (!(cin >> choice)) {
cout << "输入数字无效,请重新输入!" << endl;
// 清除错误状态
cin.clear();
// 丢弃错误输入
cin.ignore(numeric_limits<streamsize>::max(), '\n');
continue;
}

auto it = actionMap.find(choice);
if (it == actionMap.end()) {
cout << "输入数字无效,请重新输入!" << endl;
continue;
}

it->second();
}

return 0;
}

底层原理

案例代码一

模拟实现 function 类模板的功能,并调用拥有一个参数且不带返回值的函数。

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
#include <iostream>
#include <functional>
#include <string>

using namespace std;

// 定义类模板
// 这里的 Fty 代表任意函数签名,如 R(ARG)
template<typename Fty>
class myfunction {

};

/**
* @brief 模板类的特化,支持特定的函数签名 R(ARG)
* @tparam R 代表返回值类型
* @tparam ARG 代表参数类型
*/
template<typename R, typename ARG>
class myfunction<R(ARG)> {

public:
// 定义函数指针类型,指向 R(ARG) 类型的函数
using PFUNC = R(*)(ARG);

// 构造函数,接收一个函数指针,并存储起来
myfunction(PFUNC pfunc) : _pfunc(pfunc) {

}

/**
* @brief 重载小括号运算符,使对象可以像函数一样调用
* @param arg 传递给存储函数的参数
* @return 调用存储函数的返回值
*/
R operator()(ARG arg) {
return _pfunc(arg);
}

private:
// 存储函数指针
PFUNC _pfunc;

};

void hello(string str) {
cout << "hello " << str << endl;
}

int main() {
// 创建 myfunction 对象,并绑定 hello 函数
myfunction<void(string)> func1 = hello;

// 通过 func1 调用 hello
func1("peter");

return 0;
}

程序运行输出的结果如下:

1
hello peter

案例代码二

模拟实现 function 类模板的功能,并调用拥有两个参数且带返回值的函数。

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
#include <iostream>
#include <functional>
#include <string>

using namespace std;

// 定义类模板
// 这里的 Fty 代表任意函数签名,如 R(ARG1, ARG2)
template<typename Fty>
class myfunction {

};

/**
* @brief 模板类的特化,支持特定的函数签名 R(ARG1, ARG2)
* @tparam R 代表返回值类型
* @tparam ARG1 代表第一个参数类型
* @tparam ARG2 代表第二个参数类型
*/
template<typename R, typename ARG1, typename ARG2>
class myfunction<R(ARG1, ARG2)> {

public:
// 定义函数指针类型,指向 R(ARG1, ARG2) 类型的函数
using PFUNC = R(*)(ARG1, ARG2);

/**
* @brief 构造函数,接收一个函数指针,并存储起来
* @param pfunc 指向函数的指针
*/
myfunction(PFUNC pfunc) : _pfunc(pfunc) {

}

/**
* @brief 重载小括号运算符,使对象可以像函数一样调用
* @param arg1 传递给存储函数的第一个参数
* @param arg2 传递给存储函数的第二个参数
* @return 调用存储函数的返回值
*/
R operator()(ARG1 arg1, ARG2 arg2) {
return _pfunc(arg1, arg2);
}

private:
// 存储函数指针
PFUNC _pfunc;

};

int sum(int a, int b) {
return a + b;
}

int main() {
// 创建 myfunction 对象,并绑定 sum 函数
myfunction<int(int, int)> func1 = sum;

// 通过 func1 调用 sum
int result = func1(3, 5);
cout << "result: " << result << endl;

return 0;
}

程序运行输出的结果如下:

1
result: 8

案例代码三

模拟实现 function 类模板的功能,并调用拥有不同数量参数(可变参数列表)的函数,避免编写多个模板类的特例化。

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
#include <iostream>
#include <functional>
#include <string>

using namespace std;

// 定义类模板
// 这里的 Fty 代表任意函数签名,如 R(ARG...)
template<typename Fty>
class myfunction {

};

/**
* @brief myfunction 模板类的特化,支持可变长参数列表
* @tparam R 代表返回值类型
* @tparam ARG 代表参数列表(可以是任意数量的参数类型)
*/
template<typename R, typename ... ARG>
class myfunction<R(ARG...)> {

public:
// 定义函数指针类型,指向 R(ARG...) 类型的函数
using PFUNC = R(*)(ARG...);

/**
* @brief 构造函数,接收一个函数指针,并存储起来
* @param pfunc 指向函数的指针
*/
myfunction(PFUNC pfunc) : _pfunc(pfunc) {

}

/**
* @brief 重载小括号运算符,使对象可以像函数一样调用
* @param arg 传递给存储函数的参数列表
* @return 调用存储函数的返回值
*/
R operator()(ARG... arg) {
return _pfunc(arg...);
}

private:
// 存储函数指针
PFUNC _pfunc;

};

void hello(string str) {
cout << "hello " << str << endl;
}

int sum(int a, int b) {
return a + b;
}

int main() {
// 创建 myfunction 对象,并绑定 hello 函数
myfunction<void(string)> func1 = hello;

// 通过 func1 调用 hello
func1("peter");

// 创建 myfunction 对象,并绑定 sum函数
myfunction<int(int, int)> func2 = sum;

// 通过 func2 调用 sum
int result = func2(3, 4);
cout << "result: " << result << endl;

return 0;
}

程序运行输出的结果如下:

1
2
hello peter
result: 7

C++ 11 的 bind 绑定器

概念介绍

使用案例