仿函数

1.什么是仿函数

1.定义和作用

仿函数是一种重载了函数调用运算符(operator())的类或结构体,它可以像函数一样被调用。仿函数可以在很多STL算法中使用,例如sort、for_each、transform等,可以自定义排序规则、操作、条件等等。通过仿函数,C++程序员可以更加灵活地实现自己的算法。

与普通函数不同,仿函数可以保存状态,因此在使用仿函数时可以灵活地传递参数并进行计算,非常适用于一些复杂的算法和数据结构的实现。

2.仿函数与函数指针的区别

在C++中,函数指针可以作为参数传递和返回值,但是函数指针只能指向函数,无法指向类成员函数和lambda表达式。而仿函数可以作为一种通用的函数封装,可以指向函数、类成员函数以及lambda表达式,并且可以保存状态。因此,仿函数比函数指针更加灵活和可扩展。

3.仿函数的调用方式

仿函数可以像函数一样被调用

class MyFunctor {
public:
    void operator() (int i) {
        cout << i << endl;
    }
};
MyFunctor myFunctor;
myFunctor(123);
//结果
//123

在STL算法中,仿函数也可以作为函数参数传递

vector<int> vec{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
sort(vec.begin(), vec.end(), greater<int>());
for_each(vec.begin(), vec.end(), MyFunctor());

其中,greater()是一个内置的仿函数对象,用于实现降序排列。

2.仿函数的分类

1.一元仿函数和二元仿函数

一元仿函数是指只有一个参数的仿函数

template <typename T>
struct MyFunctor1 {
    void operator() (T val) {
        cout << val << endl;
    }
};
template <typename T>
struct MyFunctor2 {
    T operator() (T val) {
        return val * val;
    }
};

二元仿函数是指有两个参数的仿函数

template <typename T>
struct MyFunctor3 {
    bool operator() (T val1, T val2) {
        return val1 < val2;
    }
};

在STL算法中,一元仿函数和二元仿函数通常用于排序、查找、遍历等操作。

2.函数对象与谓词

  • 函数对象:返回值为任意类型的仿函数,例如std::plus,std::minus
  • 谓词:返回值为bool类型的仿函数,例如std::less,std::greater

3.函数适配器

函数适配器是一种特殊的仿函数,它用于将一个仿函数适配到另一个仿函数或函数对象上。STL中常用的函数适配器有:bind1st、bind2nd、not1、not2、logical_and、logical_or等

template <typename T>
struct MyFunctor4 {
    bool operator() (T val) {
        return val > 0;
    }
};
vector<int> vec{3, -1, 4, -1, 5, -9, 2, -6, 5, 3, -5};
count_if(vec.begin(), vec.end(), not1(MyFunctor4<int>()));
//结果
//3

其中,not1是一个函数适配器,它将MyFunctor4适配成一个返回相反值的仿函数对象,从而统计出vec中小于等于0的元素个数。

3.仿函数的实现

1.重载函数调用运算符

仿函数的实现首先要重载函数调用运算符(operator()),并根据需要定义参数和返回值

template <typename T>
struct MyFunctor1 {
    void operator() (T val) {
        cout << val << endl;
    }
};
template <typename T>
struct MyFunctor2 {
    T operator() (T val) {
        return val * val;
    }
};

其中,MyFunctor1是一个一元仿函数,用于输出参数值,而MyFunctor2是一个一元仿函数,用于计算参数的平方。

2.使用模板类

仿函数通常是一个模板类,可以支持不同的参数类型

template <typename T>
struct MyFunctor3 {
    bool operator() (T val1, T val2) {
        return val1 < val2;
    }
};

其中,MyFunctor3是一个二元仿函数,用于比较两个参数的大小。

3.使用函数适配器

仿函数也可以使用函数适配器来实现。例如,使用bind1st函数适配器将一个二元仿函数适配成一个一元仿函数

template <typename T>
struct MyFunctor4 {
    bool operator() (T val1, T val2) {
        return val1 < val2;
    }
};
MyFunctor4<int> myFunctor;
bind1st(myFunctor, 10)(5);
//结果
//false

其中,bind1st将myFunctor适配成一个只有一个参数的仿函数,第一个参数被绑定为10,然后用5调用这个仿函数,返回false。

4.实现一个简单的加法仿函数

class AddFunctor{
public:
    AddFunctor(int n):m_n(n){}
    int operator()(int x) const{
        return x + m_n;
    }
private:
    int m_n;
}

在上面的代码中,我们定义了一个AddFunctor类,它带有一个整型参数n,表示每次调用要加上的值。在调用运算符()时,返回x加上m_n的结果。

使用AddFunctor类,可以有如下示例

int main()
{
	AddFunctor add(3);
    int result = add(5);	//结果为8
    return 0;
}

4.仿函数的应用

在STL中,许多算法都需要使用仿函数。例如:

  • std::sort():对给定的序列进行排序,需要提供一个比较函数,用于指定排序的规则
  • std::for_each():对给定的序列中的每个元素执行指定的操作,需要提供一个函数对象,用于指定操作
  • std::find_if():在给定的序列中查找符合指定条件的第一个元素,需要提供一个谓词,用于指定条件

通过使用仿函数,我们可以将算法和数据结构解耦,是的算法更加通用和灵活。

5.仿函数的注意事项

在使用仿函数时需要注意以下几点

  • 仿函数的效率问题:由于仿函数在调用时需要进行对象的构造和析构,因此在一些需要频繁调用的场景中,使用仿函数可能会影响算法的效率
  • 仿函数的线程安全性:由于仿函数中可能会保存状态,因此在多线程环境下使用时需要注意线程安全性问题

6.仿函数的优化

1.使用constexpr

在C++11中,可以使用constexpr关键字来声明一个函数或变量是常量表达式,可以在编译时计算。如果一个仿函数的operator()是一个常量表达式,可以使用constexpr来进行优化

template <typename T>
struct MyFunctor1 {
    constexpr T operator() (T val) const {
        return val * val;
    }
};

这样,当使用MyFunctor1时,如果参数是一个常量表达式,那么编译器就可以在编译时计算出结果,从而提高程序的执行效率。

2.使用inline

仿函数可以使用inline关键字来声明为内联函数,从而在编译时将函数体直接嵌入到调用处,避免了函数调用的开销

template <typename T>
struct MyFunctor2 {
    inline T operator() (T val) const {
        return val * val;
    }
};

3.使用lambda表达式

C++11引入了lambda表达式,可以方便地定义一个匿名仿函数。与普通的仿函数相比,使用lambda表达式可以减少代码的冗余,从而提高程序的可读性和维护性

vector<int> vec{3, -1, 4, -1, 5, -9, 2, -6, 5, 3, -5};
auto count = count_if(vec.begin(), vec.end(), [](int val) { return val > 0; });

其中,lambda表达式[] (int val) { return val > 0; }定义了一个匿名仿函数,用于统计vec中大于0的元素个数。