包装器(C++11)

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 表达式是首选。
  • 如果你希望统一处理各种可调用对象,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. 绑定普通函数的一些参数,从而达到调整参数顺序或个数的效果。
  2. 绑定类的非静态成员函数到具体对象上,从而创建一个新的可调用对象。
  3. 生成可部分应用的函数

示例 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 函数的前两个参数 23,剩余的参数 c 由调用时提供。

总结

  • std::bind 是一个强大的工具,用来将函数的某些参数固定或绑定。
  • 它常用于延迟调用、生成部分应用函数以及将非静态成员函数与对象绑定。
  • 结合 占位符,我们可以灵活地控制传递参数的位置与数量,使代码更加简洁和灵活。

包装器在实践当中使用很多,功能非常强大,但也可能让代码变得复杂,所以在使用时需要小心。如果学会了就会对代码的理解有进了一步,恭喜你~ 如果文章对你有帮助的话不妨点个赞。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值