什么是C ++仿函数及其用途?

What are C++ functors and their uses?

我经常听到C++中的函子。有人能给我一个他们是什么,在什么情况下他们会有用的概述吗?


函数基本上只是定义运算符()的类。它允许您创建"看起来"像函数的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// this is a functor
struct add_x {
  add_x(int x) : x(x) {}
  int 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 class
int i = add42(8); // and"call" it
assert(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 sequence
std::transform(in.begin(), in.end(), out.begin(), add_x(1));
assert(out[i] == in[i] + 1); // for all i

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

如最后一行所示,您经常将函数作为参数传递给其他函数,如std::transform或其他标准库算法。对于常规函数指针也可以这样做,但是,正如我上面所说,函数可以"自定义",因为它们包含状态,使它们更灵活(如果我想使用函数指针,我必须编写一个函数,它的参数正好添加了1)。该函数是通用的,可以添加用它初始化的任何内容),而且它们也可能更高效。在上面的例子中,编译器确切地知道应该调用哪个函数std::transform。它应该叫add_x::operator()。这意味着它可以内联该函数调用。这使得它的效率和我手动调用向量的每个值上的函数一样高。

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


少量添加。您可以使用boost::function从函数和方法创建函数,如下所示:

1
2
3
4
5
6
7
8
9
10
11
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 functor
f(1);//prints"Foo 1"
boost::function<void (int)> b(&Bar);//wrap normal function
b(1);//prints"Bar 1"

您可以使用boost::bind向该函数添加状态

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

最有用的是,通过boost::bind和boost::function,可以从类方法创建functor,实际上这是一个委托:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 parameter
callback("useless");//prints"Hi, i am useless"

可以创建函数的列表或向量

1
2
3
4
5
6
7
std::list< boost::function<void (EventArg e)> > events;
//add some events
....
//call them
std::for_each(
        events.begin(), events.end(),
        boost::bind( boost::apply<void>(), _1, e));

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


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

1
2
3
4
5
6
7
8
class MyFunctor
{
   public:
     int operator()(int x) { return x * 2;}
}

MyFunctor doubler;
int x = doubler(5);

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

1
2
3
4
5
6
7
8
9
10
11
12
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)
{ ....}


名称"函子"在C++出现之前早就在范畴理论中被传统地使用了。这与C++的函子概念无关。最好使用name函数对象,而不是C++中所谓的"函子"。这就是其他编程语言调用类似结构的方式。

用于代替普通函数:

特征:

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

欺骗:

  • 给程序带来更多的复杂性。

使用而不是函数指针:

特征:

  • 函数对象通常可以是内联的

欺骗:

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

使用而不是虚拟函数:

特征:

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

欺骗:

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


正如其他人提到的,函数是一个像函数一样的对象,即它重载函数调用运算符。

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

1
2
3
4
5
6
7
8
9
10
11
12
class MultiplyBy {
private:
    int factor;

public:
    MultiplyBy(int x) : factor(x) {
    }

    int operator () (int other) const {
        return factor * other;
    }
};

然后您可以将MultiplyBy对象传递给类似std::transform的算法:

1
2
3
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传递函数指针,除非该调用是内联的,并且编译器知道您总是向它传递相同的函数,否则它不能通过指针内联调用。


对于像我这样的新手:经过一点研究,我发现了JALF发布的代码所做的工作。

函数是类或结构对象,可以像函数一样被"调用"。这可以通过超载() operator来实现。() operator可以接受任何数量的参数(不确定它叫什么)。其他运算符只接受两个值,即+ operator只能接受两个值(运算符的每侧各一个),并返回您为其重载的任何值。你可以在一个() operator中放入任意数量的参数,这就是它的灵活性所在。

要先创建一个函数,请先创建类。然后用您选择的类型和名称的参数为类创建一个构造函数。在同一条语句中,后面跟着一个初始值设定项列表(它使用一个冒号运算符,这也是我刚接触到的东西),它使用先前声明的构造函数参数构造类成员对象。然后() operator过载。最后,声明所创建的类或结构的私有对象。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class myFunctor
{
    public:
        /* myFunctor is the constructor. parameterVar is the parameter passed to
           the constructor. : is the initializer list operator. myObject is the
           private member object of the myFunctor class. parameterVar is passed
           to the () operator which takes it and adds it to myObject in the
           overloaded () operator function. */

        myFunctor (int parameterVar) : myObject( parameterVar ) {}

        /* the"operator" word is a keyword which indicates this function is an
           overloaded operator function. The () following this just tells the
           compiler that () is the operator being overloaded. Following that is
           the parameter for the overloaded operator. This parameter is actually
           the argument"parameterVar" passed by the constructor we just wrote.
           The last part of this statement is the overloaded operators body
           which adds the parameter passed to the member object. */

        int operator() (int myArgument) { return myObject + myArgument; }

    private:
        int myObject; //Our private member object.
};

如果这其中任何一个是不准确的或只是明显的错误,请随时纠正我!


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

1
2
3
4
5
6
7
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时,该函数接受std::vector并返回std::vector,该函数接受T并返回U。函数不必在容器类型上定义,它也可以为任何模板化类型定义,包括std::shared_ptr

1
2
3
4
5
6
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的简单示例:

1
2
3
4
5
6
7
8
9
10
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)相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 is2
std::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))相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 is2
std::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));


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

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

这是难以置信的浪费和代码复制。通常我只传入一个函数指针,然后在3个点中调用它。(所以代码只需要出现一次,而不是20次。)

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

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

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

就是这样——一个实用的例子,一个函数被证明是一个显而易见且简单的解决方案,它允许我将代码重复从20个函数减少到1个。


除了在回调中使用之外,C++函子还可以帮助提供Matrix喜欢的访问样式到矩阵类。有一个例子。


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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <string>
#include <vector>
#include

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 length
        if (m == 0) {
            return n;
        }
        if (n == 0) {
            return m;
        }
        // Size the matrix as necessary
        if (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 alone
        unsigned 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 matrix
        for (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,                 // Deletion
                    matrix_[i][j - 1] + 1,                      // Insertion
                    matrix_[i - 1][j - 1] + substitution_cost); // Substitution
            }
        }
        return matrix_[m][n];
    }
};

和重复的一样,函数是可以被视为函数的类(重载运算符())。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/* 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;
            else
                return 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提供的那样有用。


此外,我还使用了函数对象来将现有的遗留方法与命令模式相匹配(仅在我感觉到OO范式真正OCP的美妙之处);这里还添加了相关的函数适配器模式。

假设您的方法具有签名:

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

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

注意-这很难看,可能你可以使用Boost绑定助手等,但如果你不能或不想,这是一种方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// a template class for converting a member function of the type int        function(int,int,int)
//to be called as a function object
template<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来帮助上面的类调用。

1
2
3
4
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));

}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<typename _Func,typename _Ptr,typename _arg1,typename _arg2,typename _arg3>
class binder3
{
public:
//This is the constructor that does the binding part
binder3(_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 object
 void 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的helper函数:

1
2
3
4
5
6
//a helper function to call binder3
template <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);
}

现在,我们必须将其用于命令类;使用以下typedef:

1
2
3
4
5
6
7
8
9
10
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 parameter
explicit 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;
}

以下是您如何称呼它:

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

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

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


在GTKMM中使用函子将一些GUI按钮连接到一个实际的C++函数或方法。

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

1
void* method(void* something)

因此,您不能在线程中运行类中的方法(以一种简单、明显的方式),而不做额外的工作。

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

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


函数也可用于模拟在函数中定义局部函数。参考问题和其他问题。

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


我"发现"了函数的一个非常有趣的用法:当我对一个方法没有好的名称时使用它们,因为函数是一个没有名称的方法;-)