C++ 函子初相识

函子是一种特殊的对象,它可以像普通函数一样被调用 。但它又不仅仅是函数,它是一个类或者结构体的实例,通过重载operator()来实现函数调用的行为。​

普通函数大家都很熟悉,比如下面这个简单的加法函数:

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

调用的时候,直接使用add(3, 5)就可以得到结果 8。​

而函子呢,我们来看一个简单的示例:

class Adder {public:    int operator()(int a, int b) const {        return a + b;    }};

使用这个函子的时候,是这样的:

Adder adder;int result = adder(3, 5);

从使用方式上,函子和普通函数有相似之处,都可以传入参数并得到返回值。但函子有着普通函数所不具备的优势。它可以拥有自己的数据成员,这些数据成员可以在operator()重载函数中被访问和使用,从而让函子能够携带状态。这就好比一个人带着工具和记忆去完成一项任务,而普通函数就像是一个单纯执行任务的机器,没有额外的 “记忆” 和 “工具”。 例如:

class Multiplier {private:    int factor;public:    Multiplier(int f) : factor(f) {}    int operator()(int num) const {        return num * factor;    }};

这里的Multiplier函子有一个数据成员factor,在创建Multiplier对象时可以初始化这个因子,然后在调用operator()时,它会根据这个因子对传入的参数进行乘法运算。比如Multiplier multiplier(3); int result = multiplier(4);,这里result的值就是 12 ,这个3就是函子携带的状态,它会影响每次调用的结果,而普通函数很难做到这一点。

深入剖析 C++ 函子特性

1、封装处理调用

函子具有强大的封装能力,它可以将各种不同类型的调用封装起来 。这其中包括指向简单函数的指针,比如前面提到的add函数,我们可以将其指针封装在函子中;指向成员函数的指针,假设有一个类MyClass,其中有成员函数memberFunction,函子能够将指向这个成员函数的指针进行封装;还有函数对象(也就是函子自身)以及其他泛型函子等。​

更厉害的是,函子不仅能封装这些调用,还能接受它们的部分或全部参数。这在处理复杂的调用链时,展现出了极高的灵活性。比如在一个大型项目中,可能存在一系列的数据处理操作,每个操作都可以看作是一个调用。假设我们有数据读取、数据清洗、数据转换和数据存储这几个操作,每个操作都有不同的函数或函数对象来实现。我们可以使用函子将这些操作封装起来,并且可以预先设置好部分参数,比如数据存储的路径等。当需要处理数据时,只需要按照顺序调用封装好的函子,就能完成整个复杂的数据处理流程 ,而不需要每次都手动处理每个操作的参数传递和调用顺序,大大提高了代码的可维护性和可读性。

2.类型安全性

C++ 函子是类型安全的,这是它非常重要的一个特性。在 C++ 编程中,类型错误是一个常见且难以调试的问题,而函子在很大程度上避免了这个问题。因为函子在编译时就能确保不会将错误的参数类型匹配到错误的函数上。​

例如,我们有一个函子Multiply,它的operator()接受两个int类型的参数进行乘法运算:

class Multiply {public:    int operator()(int a, int b) const {        return a * b;    }};

如果在使用时,不小心将一个double类型的参数传递给这个函子,像这样Multiply multiply; double result = multiply(3.5, 4); ,编译器会在编译阶段就捕获到这个类型错误,提示参数类型不匹配。而如果是普通的函数指针调用,在运行时才发现类型错误的话,调试起来就会困难得多,可能会花费大量的时间去排查问题。所以函子的类型安全性大大减少了运行时的不确定性,让我们的代码更加健壮。

3.值语义

函子是一个具有值语义的对象,这意味着它完全支持复制、赋值和按值传递 。当我们复制一个函子时,它内部的所有数据成员都会被完整地复制,新的函子与原函子在状态上完全一致。比如:

Multiplier multiplier1(3);Multiplier multiplier2 = multiplier1;

这里multiplier2就是multiplier1的一个副本,它们都具有相同的factor值 3 。​

这种特性使得函子可以自由复制,并且不会暴露虚成员函数,从而避免了动态绑定带来的开销。在一些性能要求较高的场景中,动态绑定会增加额外的时间和空间开销,因为它需要在运行时根据对象的实际类型来确定调用哪个虚函数。而函子由于支持值语义,在编译时就可以确定调用的operator()函数,提高了执行效率。比如在一个循环中频繁调用函子进行计算,如果是具有动态绑定开销的对象,每次调用都需要进行额外的查找和判断,而函子就可以直接执行,大大提升了循环的执行速度。

函子的应用场景

1.STL 中的应用​

在 C++ 的标准模板库(STL)中,函子有着广泛且重要的应用,它就像是 STL 这座大厦中不可或缺的 “万能钥匙”,为各种算法提供了高度可定制化的能力。​

以std::sort函数为例,它是 STL 中用于对容器进行排序的强大工具。默认情况下,std::sort使用<运算符对元素进行比较,实现从小到大的排序。但在实际编程中,我们常常需要根据不同的规则进行排序,这时候函子就派上用场了。比如,我们有一个存储学生信息的vector容器,每个学生包含姓名和成绩,现在我们想要按照成绩从高到低对学生进行排序。我们可以定义一个自定义的函子:

class Student {public:    std::string name;    int score;    Student(const std::string& n, int s) : name(n), score(s) {}};class CompareByScore {public:    bool operator()(const Student& s1, const Student& s2) const {        return s1.score > s2.score;    }};

然后在使用std::sort时,将这个函子作为第三个参数传入:

std::vector<Student> students = {    {"Alice", 85},    {"Bob", 90},    {"Charlie", 78}};std::sort(students.begin(), students.end(), CompareByScore());

这样,std::sort就会根据我们定义的CompareByScore函子的规则,按照成绩从高到低对students容器中的学生进行排序。函子在 STL 中的这种应用,使得我们可以轻松地改变算法的行为,以适应各种不同的业务需求,而不需要修改std::sort的核心实现代码,大大提高了代码的复用性和可维护性 。

2.命令模式实现

命令模式是一种常见的设计模式,它的核心思想是将请求封装为对象,从而实现请求的存储和执行分离 。在这个过程中,函子扮演着关键的角色。​

假设我们正在开发一个简单的图形绘制系统,这个系统支持多种图形的绘制,比如圆形、矩形等。我们可以将每个绘制图形的操作封装成一个命令对象,而函子则可以很好地实现这个命令对象。​

首先,定义一个抽象的命令基类Command:

class Shape;class Command {public:    virtual void execute(Shape* shape) const = 0;    virtual ~Command() {}};

然后,对于绘制圆形的命令,我们可以定义一个函子类DrawCircleCommand:

class Circle : public Shape {    // 圆形的相关属性和方法};class DrawCircleCommand {public:    void operator()(Circle* circle) const {        // 实现绘制圆形的具体逻辑        std::cout << "Drawing a circle" << std::endl;    }};

在使用时,我们可以将DrawCircleCommand函子作为命令对象来使用:

Circle* circle = new Circle();DrawCircleCommand drawCircleCmd;drawCircleCmd(circle);

这里,DrawCircleCommand函子就像是一个 “命令执行者”,它将绘制圆形的操作封装起来,当调用它时,就会执行相应的绘制操作。通过这种方式,我们可以将各种绘制图形的命令存储起来,比如存储在一个容器中,然后在需要的时候按照顺序执行这些命令,实现复杂的图形绘制流程。函子在命令模式中的应用,使得代码的结构更加清晰,易于扩展和维护,每个命令的实现都可以独立进行,互不干扰。​

函数和成员的函子化转换​

1. 函数转换为函子​

在 C++ 编程中,有时我们会遇到一组具有相似签名的函数,为了更方便地管理和调用这些函数,我们可以将它们转换为函子。这样做的好处是可以将这些函数封装成一个统一的接口,提高代码的灵活性和可维护性。​

假设有一组数学运算函数,分别用于加法、减法和乘法:

int add(int a, int b) { return a + b; }int subtract(int a, int b) { return a - b; }int multiply(int a, int b) { return a * b; }

我们可以通过模板类将这些函数转换为函子:

template <typename T, T (*F)(T, T)>struct MathFunctor {    T operator()(T a, T b) const {        return F(a, b);    }};

使用时,可以这样创建函子对象:

MathFunctor<int, add> adder;MathFunctor<int, subtract> subtractor;MathFunctor<int, multiply> multiplier;int result1 = adder(3, 5);int result2 = subtractor(10, 4);int result3 = multiplier(6, 7);

通过将函数转换为函子,我们可以将这些不同的数学运算函数统一在MathFunctor这个接口下,方便在不同的场景中进行调用和管理。比如,我们可以将这些函子对象存储在一个容器中,根据不同的条件从容器中取出相应的函子进行运算,而不需要编写大量的条件判断语句来选择调用哪个函数 。​

2. 成员转换为函子​

将指向成员的指针转换为函子,能让我们把简单数据结构的成员作为参数传递给标准库算法,为数据处理带来了极大的便利。​

例如,有一个表示员工的结构体Employee,包含姓名和薪资:

struct Employee {    std::string name;    double salary;    Employee(const std::string& n, double s) : name(n), salary(s) {}};

现在我们有一个vector容器存储了多个Employee对象,想要根据薪资对员工进行排序。我们可以将salary成员转换为函子:

template <typename from_t, typename to_t, to_t from_t::* POINTER>struct DataMemberFunctor {    const to_t& operator()(const from_t& x) const {        return x.*POINTER;    }};

然后使用这个函子配合std::sort进行排序:

std::vector<Employee> employees = {    {"Alice", 5000.0},    {"Bob", 6000.0},    {"Charlie", 4500.0}};DataMemberFunctor<Employee, double, &Employee::salary> salaryGetter;std::sort(employees.begin(), employees.end(), [&salaryGetter](const Employee& e1, const Employee& e2) {    return salaryGetter(e1) < salaryGetter(e2);});

通过将成员转换为函子,我们可以轻松地利用标准库算法对结构体中的成员进行操作,避免了编写繁琐的自定义排序函数,提高了代码的简洁性和可读性。

实战练习

下面我们通过一个具体的实战案例来深入理解它的应用。假设我们有一个任务,需要对一个整数数组进行处理,找出数组中的最大值和最小值,并计算数组元素的平均值。我们可以使用函子来实现这个数据处理任务。

#include <iostream>#include <vector>#include <algorithm>// 定义一个求最大值的函子class MaxFunctor {public:    int operator()(const std::vector<int>& arr) const {        return *std::max_element(arr.begin(), arr.end());    }};// 定义一个求最小值的函子class MinFunctor {public:    int operator()(const std::vector<int>& arr) const {        return *std::min_element(arr.begin(), arr.end());    }};// 定义一个求平均值的函子class AverageFunctor {public:    double operator()(const std::vector<int>& arr) const {        int sum = 0;        for (int num : arr) {            sum += num;        }        return static_cast<double>(sum) / arr.size();    }

代码解释​

定义函子类:​

MaxFunctor类通过重载operator(),使用std::max_element算法找出数组中的最大值并返回。std::max_element是 STL 中的算法,它接受两个迭代器,表示要查找的范围,然后返回这个范围内的最大元素的迭代器,通过解引用这个迭代器(*std::max_element(arr.begin(), arr.end()))得到最大值。​

MinFunctor类类似,使用std::min_element算法找出数组中的最小值并返回 。​

AverageFunctor类在operator()中,通过遍历数组累加每个元素得到总和,然后除以数组的大小得到平均值。这里使用static_cast<double>(sum)将累加的和转换为double类型,以确保结果是浮点数,避免整数除法导致的精度损失。​

在main函数中使用函子:​

创建MaxFunctor、MinFunctor和AverageFunctor的对象maxFunctor、minFunctor和averageFunctor。​

调用这些函子对象,传入numbers数组,分别得到最大值、最小值和平均值,并输出结果。​

通过这个实战案例,我们可以看到函子在数据处理任务中的应用,它将不同的操作封装成独立的类,使得代码结构清晰,易于维护和扩展。如果后续需要添加新的数据处理操作,比如求数组元素的乘积,只需要再定义一个新的函子类即可,而不需要对原有的代码进行大规模修改。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐