什么是C++函子及其用途?

我一直听到很多关于C++中的函子的信息。有人能给我一个关于它们是什么以及在什么情况下它们会有用的概述吗?

559358 次浏览

函数是一个像函数一样的对象。基本上,定义operator()的类。

class MyFunctor{public:int operator()(int x) { return x * 2;}}
MyFunctor doubler;int x = doubler(5);

真正的优点是仿函数可以保存状态。

class Matcher{int target;public:Matcher(int m) : target(m) {}bool operator()(int x) { return x == target;}}
Matcher Is5(5);
if (Is5(n))    // same as if (n == 5){ ....}

仿函数几乎只是一个定义了运算符()的类。它允许您创建“看起来”像函数的对象:

// this is a functorstruct add_x {add_x(int val) : x(val) {}  // Constructorint operator()(int y) const { return x + y; }
private:int x;};
// Now you can use it like this:add_x add42(42); // create an instance of the functor classint i = add42(8); // and "call" itassert(i == 50); // and it added 42 to its argument
std::vector<int> in; // assume this contains a bunch of values)std::vector<int> out(in.size());// Pass a functor to std::transform, which calls the functor on every element// in the input sequence, and stores the result to the output sequencestd::transform(in.begin(), in.end(), out.begin(), add_x(1));assert(out[i] == in[i] + 1); // for all i

函子有几个好处。一个是与常规函数不同,它们可以包含状态。上面的例子创建了一个函数,它将42加到你给它的任何值。但是那个值42不是硬编码的,它是在我们创建仿函数实例时指定为构造函数参数的。我可以创建另一个加法器,它加了27,只需用不同的值调用构造函数。这使得它们可以很好地定制。

如最后几行所示,你经常将函子作为参数传递给其他函数,如std::变换或其他标准库算法。你可以用普通函数指针做同样的事情,除了,正如我上面所说,函子可以“定制”,因为它们包含状态,使它们更灵活(如果我想使用函数指针,我必须写一个函数,它的参数正好加1。仿函数是通用的,并且添加你初始化它的任何东西),它们也可能更有效。在上面的例子中,编译器确切地知道std::transform应该调用哪个函数。它应该调用add_x::operator()。这意味着它可以内联该函数调用。这使得它与我手动调用向量的每个值的函数一样有效。

如果我传递了一个函数指针,编译器不能立即看到它指向哪个函数,所以除非它执行一些相当复杂的全局优化,否则它必须在运行时取消引用指针,然后进行调用。

就像其他人提到的,仿函数是一个像函数一样的对象,即它重载函数调用操作符。

函数通常用于STL算法。它们很有用,因为它们可以在函数调用之前和之间保持状态,就像函数语言中的闭包一样。例如,您可以定义一个MultiplyBy仿函数,将其参数乘以指定数量:

class MultiplyBy {private:int factor;
public:MultiplyBy(int x) : factor(x) {}
int operator () (int other) const {return factor * other;}};

然后你可以将MultiplyBy对象传递给像std::变换这样的算法:

int array[5] = {1, 2, 3, 4, 5};std::transform(array, array + 5, array, MultiplyBy(3));// Now, array is {3, 6, 9, 12, 15}

与指向函数的指针相比,仿函数的另一个优点是调用可以在更多情况下内联。如果您将函数指针传递给transform,除非调用被内联并且编译器知道您总是将相同的函数传递给它,否则它无法通过指针内联调用。

添加很少。您可以使用#0从函数和方法创建函子,如下所示:

class Foo{public:void operator () (int i) { printf("Foo %d", i); }};void Bar(int i) { printf("Bar %d", i); }Foo foo;boost::function<void (int)> f(foo);//wrap functorf(1);//prints "Foo 1"boost::function<void (int)> b(&Bar);//wrap normal functionb(1);//prints "Bar 1"

你可以使用::bind向这个仿函数添加状态

boost::function<void ()> f1 = boost::bind(foo, 2);f1();//no more argument, function argument stored in f1//and this print "Foo 2" (://and normal functionboost::function<void ()> b1 = boost::bind(&Bar, 2);b1();// print "Bar 2"

而且最有用的是,使用boop::bind和boop::function,您可以从class方法创建仿函数,实际上这是一个委托:

class SomeClass{std::string state_;public:SomeClass(const char* s) : state_(s) {}
void method( std::string param ){std::cout << state_ << param << std::endl;}};SomeClass *inst = new SomeClass("Hi, i am ");boost::function< void (std::string) > callback;callback = boost::bind(&SomeClass::method, inst, _1);//create delegate//_1 is a placeholder it holds plase for parametercallback("useless");//prints "Hi, i am useless"

您可以创建函子列表或向量

std::list< boost::function<void (EventArg e)> > events;//add some events....//call themstd::for_each(events.begin(), events.end(),boost::bind( boost::apply<void>(), _1, e));

所有这些东西都有一个问题,编译器错误消息不是人类可读的:)

名称“仿函数”早在C++出现之前就已经在范畴理论中使用了。这与C++仿函数的概念无关。最好使用名称函数对象而不是我们在C++中所说的“仿函数”。这就是其他编程语言调用类似结构的方式。

使用而不是普通函数:

产品特点:

  • 函数对象可以有状态
  • 函数对象适合OOP(它的行为与其他对象一样)。

缺点:

  • 增加了程序的复杂性。

用于代替函数指针:

产品特点:

  • 函数对象通常可以内联

缺点:

  • 函数对象在运行时不能与其他函数对象类型交换(至少除非它扩展了一些基类,因此会产生一些开销)

用于代替虚函数:

产品特点:

  • 函数对象(非虚拟)不需要vtable和运行时调度,因此在大多数情况下效率更高

缺点:

  • 函数对象在运行时不能与其他函数对象类型交换(至少除非它扩展了一些基类,因此会产生一些开销)

函数在gtkmm中用于将一些GUI按钮连接到实际的C++函数或方法。


如果您使用p线程库使您的应用程序多线程,函数可以帮助您。
要启动一个线程,pthread_create(..)的参数之一是要在他自己的线程上执行的函数指针。
但是有一个不便。这个指针不能成为指向方法的指针,除非它是静态方法,或者除非你是指定它的类,比如class::method。还有一件事,你的方法的接口只能是:

void* method(void* something)

因此,如果不做额外的事情,您就不能(以简单明显的方式)在线程中运行类中的方法。

在C++中处理线程的一个非常好的方法是创建自己的Thread类。如果你想运行MyClass类的方法,我所做的就是将这些方法转换为Functor派生类。

另外,Thread类有这个方法:static void* startThread(void* arg)
指向此方法的指针将用作调用pthread_create(..)的参数。startThread(..)在arg中应该接收的是void*对任何Functor派生类堆中实例的强制转换引用,执行时将强制转换回Functor*,然后调用它的run()方法。

这是一个实际情况,我被迫使用函数来解决我的问题:

我有一组函数(比如说,其中20个),它们都是相同的,除了每个函数在3个特定位置调用不同的特定函数。

这是令人难以置信的浪费和代码重复。通常我会传入一个函数指针,并在3个位置调用它。(所以代码只需要出现一次,而不是20次。)

但后来我意识到,在每种情况下,特定的函数需要一个完全不同的参数配置文件!有时2个参数,有时5个参数,等等。

另一种解决方案是有一个基类,其中特定的函数是派生类中的重写方法。但是我真的想构建所有这些继承吗,只是为了传递函数指针???

解决方案:所以我做的是,我做了一个包装类(一个“Functor”),它能够调用我需要调用的任何函数。我提前设置了它(包括它的参数等),然后我传递它而不是函数指针。现在被调用的代码可以触发Functor,而不知道内部发生了什么。它甚至可以多次调用它(我需要它调用3次。)


就是这样——一个实际的例子,一个函数被证明是明显而简单的解决方案,它使我能够将代码重复从20个函数减少到1个。

除了用于回调之外,C++函子还可以帮助为矩阵类提供matlab喜欢的访问样式。有一个示例

对于像我这样的新手:经过一点研究,我弄清楚了jalf发布的代码是做什么的。

仿函数是可以像函数一样“调用”的类或结构对象。这可以通过重载() operator来实现。() operator(不确定它的调用)可以接受任意数量的参数。其他运算符只接受两个,即+ operator只能接受两个值(运算符的每一侧一个)并返回你重载它的任何值。你可以在() operator中容纳任意数量的参数,这就是它的灵活性。

要创建一个仿函数,首先要创建你的类。然后你用你选择的类型和名称的参数为类创建一个构造函数。在同一个语句中,后面跟着一个初始化器列表(它使用单个冒号运算符,我也是新手),它用前面声明的参数构造类成员对象到构造函数。然后() operator被重载。最后你声明你创建的类或结构的私有对象。

我的代码(我发现jalf的变量名令人困惑)

class myFunctor{public:/* myFunctor is the constructor. parameterVar is the parameter passed tothe constructor. : is the initializer list operator. myObject is theprivate member object of the myFunctor class. parameterVar is passedto the () operator which takes it and adds it to myObject in theoverloaded () operator function. */myFunctor (int parameterVar) : myObject( parameterVar ) {}
/* the "operator" word is a keyword which indicates this function is anoverloaded operator function. The () following this just tells thecompiler that () is the operator being overloaded. Following that isthe parameter for the overloaded operator. This parameter is actuallythe argument "parameterVar" passed by the constructor we just wrote.The last part of this statement is the overloaded operators bodywhich adds the parameter passed to the member object. */int operator() (int myArgument) { return myObject + myArgument; }
private:int myObject; //Our private member object.};

如果任何这是不准确的或只是简单的错误,请随时纠正我!

为了补充,我使用函数对象将现有的遗留方法与命令模式相适应;(只有在OO范例的美丽之处我才感受到真正的OCP);还在这里添加了相关的函数适配器模式。

假设您的方法具有签名:

int CTask::ThreeParameterTask(int par1, int par2, int par3)

我们将看到如何使它适合命令模式-为此,首先,您必须编写一个成员函数适配器,以便可以将其作为函数对象调用。

注意-这是丑陋的,也许你可以使用Boost绑定助手等,但如果你不能或不想,这是一种方法。

// a template class for converting a member function of the type int        function(int,int,int)//to be called as a function objecttemplate<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>class mem_fun3_t{public:explicit mem_fun3_t(_Ret (_Class::*_Pm)(_arg1,_arg2,_arg3)):m_Ptr(_Pm) //okay here we store the member function pointer for later use{}
//this operator call comes from the bind method_Ret operator()(_Class *_P, _arg1 arg1, _arg2 arg2, _arg3 arg3) const{return ((_P->*m_Ptr)(arg1,arg2,arg3));}private:_Ret (_Class::*m_Ptr)(_arg1,_arg2,_arg3);// method pointer signature};

此外,我们需要一个帮助方法mem_fun3来帮助调用上述类。

template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3> mem_fun3 ( _Ret (_Class::*_Pm)          (_arg1,_arg2,_arg3) ){return (mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3>(_Pm));}

现在,为了绑定参数,我们必须编写一个绑定函数。所以,这里是:

template<typename _Func,typename _Ptr,typename _arg1,typename _arg2,typename _arg3>class binder3{public://This is the constructor that does the binding partbinder3(_Func fn,_Ptr ptr,_arg1 i,_arg2 j,_arg3 k):m_ptr(ptr),m_fn(fn),m1(i),m2(j),m3(k){}
//and this is the function objectvoid operator()() const{m_fn(m_ptr,m1,m2,m3);//that calls the operator}private:_Ptr m_ptr;_Func m_fn;_arg1 m1; _arg2 m2; _arg3 m3;};

并且,使用binder3类的辅助函数-bind3

//a helper function to call binder3template <typename _Func, typename _P1,typename _arg1,typename _arg2,typename _arg3>binder3<_Func, _P1, _arg1, _arg2, _arg3> bind3(_Func func, _P1 p1,_arg1 i,_arg2 j,_arg3 k){return binder3<_Func, _P1, _arg1, _arg2, _arg3> (func, p1,i,j,k);}

现在,我们必须将其与Command类一起使用;使用以下typedef:

typedef binder3<mem_fun3_t<int,T,int,int,int> ,T* ,int,int,int> F3;//and change the signature of the ctor//just to illustrate the usage with a method signature taking more than one parameterexplicit Command(T* pObj,F3* p_method,long timeout,const char* key,long priority = PRIO_NORMAL ):m_objptr(pObj),m_timeout(timeout),m_key(key),m_value(priority),method1(0),method0(0),method(0){method3 = p_method;}

这就是你如何称呼它:

F3 f3 = PluginThreadPool::bind3( PluginThreadPool::mem_fun3(&CTask::ThreeParameterTask), task1,2122,23 );

注意:f3();将调用方法task1->ThreeParameterTask(21,22,23);

此模式的完整上下文位于以下链接

仿函数是高阶函数,它将函数应用于参数化(即模板化)类型。它是地图高阶函数的泛化。例如,我们可以为std::vector定义一个仿函数,如下所示:

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>std::vector<U> fmap(F f, const std::vector<T>& vec){std::vector<U> result;std::transform(vec.begin(), vec.end(), std::back_inserter(result), f);return result;}

当给定函数F接受T并返回U时,此函数接受std::vector<T>并返回std::vector<U>。仿函数不必在容器类型上定义,它也可以定义为任何模板类型,包括std::shared_ptr

template<class F, class T, class U=decltype(std::declval<F>()(std::declval<T>()))>std::shared_ptr<U> fmap(F f, const std::shared_ptr<T>& p){if (p == nullptr) return nullptr;else return std::shared_ptr<U>(new U(f(*p)));}

下面是一个将类型转换为double的简单示例:

double to_double(int x){return x;}
std::shared_ptr<int> i(new int(3));std::shared_ptr<double> d = fmap(to_double, i);
std::vector<int> is = { 1, 2, 3 };std::vector<double> ds = fmap(to_double, is);

函子应该遵循两个定律。第一个是恒等定律,它指出如果给仿函数一个恒等函数,它应该与将恒等函数应用于类型相同,即fmap(identity, x)应该与identity(x)相同:

struct identity_f{template<class T>T operator()(T x) const{return x;}};identity_f identity = {};
std::vector<int> is = { 1, 2, 3 };// These two statements should be equivalent.// is1 should equal is2std::vector<int> is1 = fmap(identity, is);std::vector<int> is2 = identity(is);

下一个定律是组合定律,它指出如果仿函数由两个函数组成,它应该与对第一个函数应用仿函数然后再次应用第二个函数相同。所以,fmap(std::bind(f, std::bind(g, _1)), x)应该与fmap(f, fmap(g, x))相同:

double to_double(int x){return x;}
struct foo{double x;};
foo to_foo(double x){foo r;r.x = x;return r;}
std::vector<int> is = { 1, 2, 3 };// These two statements should be equivalent.// is1 should equal is2std::vector<foo> is1 = fmap(std::bind(to_foo, std::bind(to_double, _1)), is);std::vector<foo> is2 = fmap(to_foo, fmap(to_double, is));

就像已经重复的那样,函子是可以被视为函数的类(重载运算符())。

它们对于需要将某些数据与重复或延迟的函数调用相关联的情况最有用。

例如,函子链表可用于实现基本的低开销同步协程系统、任务调度程序或可中断文件解析。示例:

/* prints "this is a very simple and poorly used task queue" */class Functor{public:std::string output;Functor(const std::string& out): output(out){}operator()() const{std::cout << output << " ";}};
int main(int argc, char **argv){std::list<Functor> taskQueue;taskQueue.push_back(Functor("this"));taskQueue.push_back(Functor("is a"));taskQueue.push_back(Functor("very simple"));taskQueue.push_back(Functor("and poorly used"));taskQueue.push_back(Functor("task queue"));for(std::list<Functor>::iterator it = taskQueue.begin();it != taskQueue.end(); ++it){*it();}return 0;}
/* prints the value stored in "i", then asks you if you want to increment it */int i;bool should_increment;int doSomeWork(){std::cout << "i = " << i << std::endl;std::cout << "increment? (enter the number 1 to increment, 0 otherwise" << std::endl;std::cin >> should_increment;return 2;}void doSensitiveWork(){++i;should_increment = false;}class BaseCoroutine{public:BaseCoroutine(int stat): status(stat), waiting(false){}void operator()(){ status = perform(); }int getStatus() const { return status; }protected:int status;bool waiting;virtual int perform() = 0;bool await_status(BaseCoroutine& other, int stat, int change){if(!waiting){waiting = true;}if(other.getStatus() == stat){status = change;waiting = false;}return !waiting;}}
class MyCoroutine1: public BaseCoroutine{public:MyCoroutine1(BaseCoroutine& other): BaseCoroutine(1), partner(other){}protected:BaseCoroutine& partner;virtual int perform(){if(getStatus() == 1)return doSomeWork();if(getStatus() == 2){if(await_status(partner, 1))return 1;else if(i == 100)return 0;elsereturn 2;}}};
class MyCoroutine2: public BaseCoroutine{public:MyCoroutine2(bool& work_signal): BaseCoroutine(1), ready(work_signal) {}protected:bool& work_signal;virtual int perform(){if(i == 100)return 0;if(work_signal){doSensitiveWork();return 2;}return 1;}};
int main(){std::list<BaseCoroutine* > coroutineList;MyCoroutine2 *incrementer = new MyCoroutine2(should_increment);MyCoroutine1 *printer = new MyCoroutine1(incrementer);
while(coroutineList.size()){for(std::list<BaseCoroutine *>::iterator it = coroutineList.begin();it != coroutineList.end(); ++it){*it();if(*it.getStatus() == 0){coroutineList.erase(it);}}}delete printer;delete incrementer;return 0;}

当然,这些例子本身并没有那么有用。它们只展示了函子是如何有用的,函子本身是非常基本和不灵活的,这使得它们不如Boost提供的有用。

函数也可用于模拟在函数中定义本地函数。请参阅问题另一个

但是局部仿函数不能访问外部自动变量。lambda(C++11)函数是一个更好的解决方案。

将函数实现为函子的一大优点是它们可以在调用之间维护和重用状态。例如,许多动态编程算法,如用于计算字符串之间列文施泰因距离Wagner-Fischer算法,通过填写一个大的结果表来工作。每次调用函数时分配这个表是非常低效的,因此将函数实现为仿函数并使表成为成员变量可以大大提高性能。

下面是将Wagner-Fischer算法实现为仿函数的示例。请注意如何在构造函数中分配表,然后在operator()中重用,并根据需要调整大小。

#include <string>#include <vector>#include <algorithm>
template <typename T>T min3(const T& a, const T& b, const T& c){return std::min(std::min(a, b), c);}
class levenshtein_distance{mutable std::vector<std::vector<unsigned int> > matrix_;
public:explicit levenshtein_distance(size_t initial_size = 8): matrix_(initial_size, std::vector<unsigned int>(initial_size)){}
unsigned int operator()(const std::string& s, const std::string& t) const{const size_t m = s.size();const size_t n = t.size();// The distance between a string and the empty string is the string's lengthif (m == 0) {return n;}if (n == 0) {return m;}// Size the matrix as necessaryif (matrix_.size() < m + 1) {matrix_.resize(m + 1, matrix_[0]);}if (matrix_[0].size() < n + 1) {for (auto& mat : matrix_) {mat.resize(n + 1);}}// The top row and left column are prefixes that can be reached by// insertions and deletions aloneunsigned int i, j;for (i = 1;  i <= m; ++i) {matrix_[i][0] = i;}for (j = 1; j <= n; ++j) {matrix_[0][j] = j;}// Fill in the rest of the matrixfor (j = 1; j <= n; ++j) {for (i = 1; i <= m; ++i) {unsigned int substitution_cost = s[i - 1] == t[j - 1] ? 0 : 1;matrix_[i][j] =min3(matrix_[i - 1][j] + 1,                 // Deletionmatrix_[i][j - 1] + 1,                      // Insertionmatrix_[i - 1][j - 1] + substitution_cost); // Substitution}}return matrix_[m][n];}};