1. 三种可调用对象
在学习包装器之前,先回顾一下C++中三种用于定义可调用对象的方式:函数指针、仿函数(即函数对象)和 lambda 表达式。它们各有优缺点,适用于不同的场景。
a. 函数指针
- 函数指针是指向函数的指针,可以通过它来调用函数。
- 优点:
- 使用起来简单直接,适合指向独立的函数。
- 支持将不同函数传递给相同的调用接口,非常灵活。
- 缺点:
- 函数指针只能指向全局或静态函数,无法指向类的非静态成员函数。
- 类型定义复杂,且没有类型检查的优势,容易导致意外调用错误函数。
- 不支持状态维护,无法保存上下文信息。
b. 仿函数(函数对象)
- 仿函数是通过重载
operator()
运算符的类或结构体,允许对象像函数一样调用。 - 优点:
- 可以在类中保存状态(例如类的成员变量),支持复杂的逻辑。
- 类型安全,编译时检查更严格。
- 可以通过模板进行泛型编程,使用灵活。
- 缺点:
- 实现上比函数指针复杂,需要定义一个类和重载
()
操作符,不适合统一类型。 - 因为仿函数对象可以存储状态,内存消耗可能更大。
- 实现上比函数指针复杂,需要定义一个类和重载
c. Lambda 表达式
- Lambda 表达式是 C++11 引入的一种匿名函数,支持通过捕获列表将上下文变量引入到表达式中。
- 优点:
- 简洁:可以在函数内部定义临时的可调用对象,无需显式声明函数或类。
- 灵活:支持通过捕获列表捕获外部变量,可以是按值或按引用捕获。
- 支持泛型:可以与
auto
、模板结合使用。 - 更好的内联优化:因为 lambda 是内联的,编译器可以对其进行更多的优化。
- 缺点:
- 不适合过于复杂的逻辑,因为会让代码难以阅读和维护。
- C++11 引入,老版本的编译器不支持。
为什么引入包装器?
- 概念:
std::function
是一个通用的函数包装器,可以存储任何可以调用的目标,包括函数指针、仿函数和 lambda 表达式。 - 引入的原因:
- 统一接口:函数指针、仿函数和 lambda 各有不同的语法和特点,
std::function
允许我们使用统一的方式处理这些不同的可调用对象。它让代码更加灵活,并简化了接口设计。 - 类型安全:
std::function
提供类型安全的调用机制,避免了函数指针的潜在问题。 - 支持存储状态:与函数指针不同,
std::function
能够存储可调用对象的状态,例如捕获的 lambda 变量或仿函数对象的状态。
- 统一接口:函数指针、仿函数和 lambda 各有不同的语法和特点,
总结:
- 如果你需要简单地调用一个全局函数,函数指针足够了。
- 如果你需要一个保存状态且可调用的对象,选择仿函数对象。
- 当需要临时的、简洁的可调用对象时,lambda 表达式是首选。
- 如果你希望统一处理各种可调用对象,
std::function
这种包装器能提供极大的便利。
2. function基础使用
C++中的function就是一种函数包装器,其本质也是一个类模板:
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
Ret
是可调用对象返回值的类型Args
是可调用对象的参数类型
下面是一个示例代码,展示如何将不同的可调用对象(函数指针、仿函数、lambda 表达式)传递给相同的调用接口,并展示 std::function
的灵活性。
#include <iostream>
#include <functional>
// 定义函数:接受整数并输出其平方
void square(int x) {
std::cout << "Square: " << x * x << std::endl;
}
// 定义仿函数:计算整数的立方
struct Cube {
void operator()(int x) const {
std::cout << "Cube: " << x * x * x << std::endl;
}
};
// 统一的调用接口,接受std::function类型的可调用对象
void call_with_10(const std::function<void(int)>& func) {
func(10); // 将10传递给可调用对象
}
int main() {
// 1. 使用函数指针
std::function<void(int)> func_ptr = square;
call_with_10(func_ptr); // 输出 Square: 100
// 2. 使用仿函数对象
std::function<void(int)> functor = Cube();
call_with_10(functor); // 输出 Cube: 1000
// 3. 使用lambda表达式
std::function<void(int)> lambda = [](int x) {
std::cout << "Lambda: " << x + 5 << std::endl;
};
call_with_10(lambda); // 输出 Lambda: 15
return 0;
}
不仅仅是可以调用以上的可调用对象,类的静态和非静态成员函数都可以,以下为具体示例:
示例代码
#include <iostream>
#include <functional>
// 定义一个简单的类
class MyClass {
public:
static int staticVar;
int nonStaticVar;
static void staticFunction(int x) {
std::cout << "Static Function: staticVar = " << staticVar << ", x = " << x << std::endl;
}
void nonStaticFunction(int x) {
std::cout << "Non-static Function: nonStaticVar = " << nonStaticVar << ", x = " << x << std::endl;
}
};
// 初始化静态成员变量
int MyClass::staticVar = 42;
int main() {
// 1. 调用静态成员函数
MyClass::staticFunction(10); // 直接通过类名调用,不需要实例化对象
// 2. 创建对象,调用非静态成员函数
MyClass obj;
obj.nonStaticVar = 5;
obj.nonStaticFunction(10); // 通过对象调用非静态成员函数
// 3. 使用 std::function 绑定静态成员函数
std::function<void(int)> staticFuncPtr = &MyClass::staticFunction;
staticFuncPtr(20); // 可以通过 std::function 调用静态成员函数
// 4. 使用 std::function 绑定非静态成员函数
std::function<void(MyClass&, int)> nonStaticFuncPtr = &MyClass::nonStaticFunction;
nonStaticFuncPtr(obj, 30); // 必须传递对象实例作为第一个参数
return 0;
}
代码分析:
使用 std::function
绑定成员函数:
- 静态成员函数:因为静态成员函数不依赖于对象,可以直接通过
std::function
绑定并调用,类似于普通函数指针。std::function<void(int)> staticFuncPtr = MyClass::staticFunction; staticFuncPtr(20);
- 非静态成员函数:非静态成员函数必须绑定到一个对象实例上,才能被调用。因此,
std::function
的第一个参数必须传递对象实例。std::function<void(MyClass&, int)> nonStaticFuncPtr = &MyClass::nonStaticFunction; nonStaticFuncPtr(obj, 30);
总结:
- 静态成员函数与类本身关联,不依赖于对象,可以直接通过类名调用。
- 非静态成员函数与对象关联,必须通过对象实例调用。
- 通过
std::function
可以统一管理和调用这些函数,但非静态成员函数需要额外传递对象实例作为参数。取静态成员函数的地址可以不用取地址运算符&
,但取非静态成员函数的地址必须使用取地址运算符&
,一般都可以写上。
3. 简化代码
为了更好地理解包装器的作用,下面我们通过一个具体的例子来展示如何使用包装器(例如 std::function
)来简化代码,尤其是在处理不同类型的可调用对象(函数指针、仿函数、lambda 表达式)时的简化效果。
场景说明
假设我们要实现一个事件处理系统,允许用户注册不同的回调函数来处理某个事件。这些回调函数可以是普通的函数、类的成员函数(包括静态和非静态),也可以是 lambda 表达式或仿函数。为了统一管理这些不同的可调用对象,我们可以使用 std::function
作为包装器。
前后的代码对比
a. 没有包装器的代码(手动区分不同的可调用对象)
#include <iostream>
// 处理函数指针
void processFunctionPointer(void (*callback)(int)) {
callback(42); // 执行函数指针
}
// 处理类的静态成员函数
void processStaticMember(void (*callback)(int)) {
callback(42); // 执行静态成员函数
}
// 处理类的非静态成员函数
void processNonStaticMember(void (MyClass::*callback)(int), MyClass& obj) {
(obj.*callback)(42); // 执行非静态成员函数
}
class MyClass {
public:
void nonStaticFunction(int x) {
std::cout << "Non-static function: " << x << std::endl;
}
static void staticFunction(int x) {
std::cout << "Static function: " << x << std::endl;
}
};
// 普通函数
void normalFunction(int x) {
std::cout << "Normal function: " << x << std::endl;
}
int main() {
MyClass obj;
processFunctionPointer(normalFunction);
processStaticMember(MyClass::staticFunction);
processNonStaticMember(&MyClass::nonStaticFunction, obj);
return 0;
}
- 需要为每种类型的可调用对象(函数指针、静态成员函数、非静态成员函数)编写单独的处理函数,增加了代码复杂度。
b. 使用 function 的代码(简化代码)
#include <iostream>
#include <functional> // 引入 std::function
class MyClass {
public:
void nonStaticFunction(int x) {
std::cout << "Non-static function: " << x << std::endl;
}
static void staticFunction(int x) {
std::cout << "Static function: " << x << std::endl;
}
};
// 普通函数
void normalFunction(int x) {
std::cout << "Normal function: " << x << std::endl;
}
// 通用处理函数,使用std::function简化代码,统一调用接口
void processEvent(const std::function<void(int)>& callback) {
callback(42);
}
int main() {
MyClass obj;
processEvent(normalFunction);
processEvent(MyClass::staticFunction);
processEvent(std::bind(&MyClass::nonStaticFunction, &obj, std::placeholders::_1));
// bind这块后面会解释
processEvent([](int x) { std::cout << "Lambda: " << x << std::endl; });
return 0;
}
通过使用 std::function
这样的包装器,我们可以将不同类型的可调用对象统一管理,并且简化了回调函数的处理逻辑,使代码更简洁、灵活和可扩展。这也充分体现了包装器在实际开发中的作用。
4. bind的基本使用
std::bind
是 C++ 标准库中的一个函数模板,它允许你将函数的一些参数提前绑定,生成一个新的可调用对象。std::bind
可以绑定普通函数、成员函数、以及其他可调用对象,常用于生成部分应用函数或将成员函数绑定到对象实例上。可见,bind
的本质就是一个被封装过的一个类模板,会根据传入的函数和参数列表自动生成。
语法
auto boundFunction = std::bind(callable, arg1, arg2, ..., std::placeholders::_1, ...);
- callable: 可调用对象,如普通函数、成员函数、仿函数或 lambda。
- arg1, arg2,…: 需要提前绑定的参数。
- std::placeholders::_1, std::placeholders::_2,…: 表示参数占位符,表示调用时传递的参数会被放置在对应的位置。
主要用途
- 绑定普通函数的一些参数,从而达到调整参数顺序或个数的效果。
- 绑定类的非静态成员函数到具体对象上,从而创建一个新的可调用对象。
- 生成可部分应用的函数。
示例 1:绑定普通函数
#include <iostream>
#include <functional>
void printSum(int a, int b) {
std::cout << "Sum: " << a + b << std::endl;
}
int main() {
// 绑定第一个参数为 10
auto boundFunc = std::bind(printSum, 10, std::placeholders::_1);
boundFunc(5); // 输出 Sum: 15
return 0;
}
分析:
std::bind(printSum, 10, std::placeholders::_1)
创建了一个新函数boundFunc
,其中printSum
的第一个参数被固定为 10,第二个参数由调用时提供。- 当
boundFunc(5)
被调用时,相当于printSum(10, 5)
。
示例 2:绑定非静态成员函数
#include <iostream>
#include <functional>
class MyClass {
public:
void display(int x) {
std::cout << "Value: " << x << std::endl;
}
};
int main() {
MyClass obj;
// 绑定对象 obj 到成员函数 display
auto boundFunc = std::bind(&MyClass::display, &obj, std::placeholders::_1);
boundFunc(100); // 输出 Value: 100
return 0;
}
分析:
std::bind(&MyClass::display, &obj, std::placeholders::_1)
将成员函数display
绑定到对象obj
,生成了一个可以直接调用的函数boundFunc
。- 调用
boundFunc(100)
时,相当于执行obj.display(100)
。
示例 3:绑定多个参数
#include <iostream>
#include <functional>
void multiply(int a, int b, int c) {
std::cout << "Result: " << a * b * c << std::endl;
}
int main() {
// 绑定 a = 2, b = 3,c 由调用时提供
auto boundFunc = std::bind(multiply, 2, 3, std::placeholders::_1);
boundFunc(4); // 输出 Result: 24 (2 * 3 * 4)
return 0;
}
std::bind(multiply, 2, 3, std::placeholders::_1)
绑定了 multiply
函数的前两个参数 2
和 3
,剩余的参数 c
由调用时提供。
总结
std::bind
是一个强大的工具,用来将函数的某些参数固定或绑定。- 它常用于延迟调用、生成部分应用函数以及将非静态成员函数与对象绑定。
- 结合 占位符,我们可以灵活地控制传递参数的位置与数量,使代码更加简洁和灵活。
包装器在实践当中使用很多,功能非常强大,但也可能让代码变得复杂,所以在使用时需要小心。如果学会了就会对代码的理解有进了一步,恭喜你~ 如果文章对你有帮助的话不妨点个赞。