C++函数对象是什么?它们有什么用途?

1068

我一直听到在C++中有关functor的许多内容。能否给我一个概述,告诉我它们是什么以及在什么情况下它们会有用?


5
这个主题已经在回答这个问题时进行了讨论:https://dev59.com/_nRC5IYBdhLWcg3wYP2h - Luc Touraille
2
它用于在C++中创建闭包。 - copper.hat
5
看下面的回答,如果有人想知道operator()(...)是什么意思:它是重载_"函数调用"_运算符。这只是对()运算符进行操作符重载。不要将operator()与调用名为operator的函数混淆,而是将其视为常规的运算符重载语法。 - zardosht
非常相关:为什么要重载 operator() - Gabriel Staples
14个回答

1212

一个函数对象(functor)基本上就是一个定义了operator()的类。这使得你可以创建像函数一样的对象:

// this is a functor
struct add_x {
  add_x(int val) : x(val) {}  // Constructor
  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

关于函数对象(functor)有几个好处。其中一个是,与普通函数不同,它们可以包含状态。上面的例子创建了一个函数,它会将给定的值加上42。但是这个值42并不是硬编码的,在创建函数对象实例时,我们通过构造函数参数指定了它。我可以通过使用不同的值调用构造函数来创建另一个加法器,比如加27。这使得它们非常灵活可定制。

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

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

45
请问您能解释一下这行代码吗?std::transform(in.begin(), in.end(), out.begin(), add_x(1)); 为什么您在这里写的是add_x而不是add42呢? - Alecs
131
@Alecs,两种方法都可行(但效果不同)。如果我使用了add42,我将使用之前创建的函数对象,并将42添加到每个值。而使用add_x(1),我会创建一个新的函数对象实例,它只会将1添加到每个值。这只是为了说明通常情况下,你会在需要时“即兴”创建函数对象,而不是先创建并在实际使用它之前一直保留它。 - jalf
8
当然可以。它们只需要拥有 operator(),因为这是调用者使用的方法。除此之外,函数对象拥有的其他成员函数、构造函数、操作符和成员变量完全由您决定。 - jalf
5
在函数式编程的术语中,你是正确的,一个函数也是一个函子,但在C++的术语中,函子特指用作函数的类。这个术语早期有点滥用,但这种区分是有用的,因此今天仍然存在。如果你在C++的上下文中开始将函数称为“函子”,那么你只会让对话变得混乱。 - srm
7
是一个类还是类的实例?在大多数资料中,add42会被称为函数对象,而不是add_x(它是函数对象的类或者简称函数对象类)。我认为这种术语使用是一致的,因为函数对象也被称为“函数对象”,而不是函数类。请问您是否需要澄清此点? - Sergei Tachenov
显示剩余16条评论

131

小加说明,您可以使用boost::function来从函数和方法中创建函数对象,例如:

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 为这个函数对象添加状态

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,您可以从类方法创建函数对象,实际上这就是一个委托:

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"

你可以创建一个函数对象列表或向量。

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));

所有这些东西都存在一个问题,编译器错误信息不易读懂 :)


4
第一个例子中operator()是否应该是public的,因为类默认为private? - NathanOliver
9
也许有一天这个答案值得更新,因为现在Lambda表达式是从任何东西获得函数符最简单的方法。 - 463035818_is_not_a_number
1
在C++11中,有std::functionstd::bind - phuclv

130

函数对象是一个表现得像函数的对象。基本上,它是定义了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)
{ ....}

13
只需补充一点,它们可以像函数指针一样使用。 - Martin York
11
对于那些对这个概念还不熟悉的人来说,可能会有点误导性。Functor可以"像使用"函数指针一样使用,但并不总是能够"代替"函数指针。例如,一个需要函数指针作为参数的函数不能用functor代替,即使functor的参数和返回值与函数指针相同。但总的来说,在设计时,functor是更受欢迎和理论上更"现代"的选择。 - MasonWinsauer
@QPaysTaxes 可能是个打字错误。我可能从第一个例子中复制并粘贴了代码,然后忘记更改它。现在我已经修复了它。 - James Curran
使用bool Is5(int n) {return 5==n;}的优势是什么? - StarDust
1
如果Matcher在一个库中,定义Is5()就非常简单。而且你可以创建Is7()、Is32()等等。此外,这只是一个例子。Functor可能会更加复杂。 - James Curran
显示剩余2条评论

68

在C++出现之前,"functor"这个名称已经在范畴论中传统地使用了很久。这与C++中的"functor"概念无关。最好使用名称函数对象来代替我们在C++中所称呼的"functor",因为其他编程语言也是这样称呼类似的构造体。

与普通函数相比,它的特点是:

  • 函数对象可以具有状态
  • 函数对象适用于面向对象编程(它的行为和其他对象一样)。

缺点:

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

与函数指针相比,它的特点是:

  • 函数对象通常可以内联

缺点:

  • 函数对象在运行时不能与其他函数对象类型交换(除非它扩展了某个基类,这将导致一些开销)

与虚函数相比,它的特点是:

  • 非虚函数对象不需要vtable和运行时分派,因此在大多数情况下更高效

缺点:

  • 函数对象在运行时不能与其他函数对象类型交换(除非它扩展了某个基类,这将导致一些开销)

2
你能用实际例子解释一下这些用例吗?我们如何将函数对象用作多态和函数指针? - Milad Khajavi
1
一个函数对象持有状态到底意味着什么? - erogol
感谢指出需要一个基类才能实现某种形式的多态。我遇到的问题是必须在同一位置使用函数指针和函数对象,而我找到的唯一方法是编写一个函数对象基类(因为我不能使用C++11的东西)。直到看到你的回答,我才确定这种开销是否有意义。 - 463035818_is_not_a_number
1
@Erogol 一个函数对象是一个支持语法foo(arguments)的对象。因此,它可以包含变量;例如,如果您有一个update_password(string)函数,您可能想要跟踪它发生的频率;使用函数对象,可以使用一个表示时间戳的private long time来表示最后一次发生的时间。使用函数指针或普通函数,您需要在其命名空间之外使用一个变量,这仅通过文档和用法直接相关,而不是通过定义相关。 - anon
6
谢谢您的请求。以下是翻译的结果:⁺¹表示提到这个名字是毫无意义地编造出来的。我正在寻找数学(或者如果你愿意,是函数式)函子与C++中的函子之间的关系。 - Hi-Angel
感谢您详细阐述了“functor”这个词的误用。 - copper.hat

46

像其他人提到的那样,functor 是一个表现得像函数的对象,即它重载了函数调用运算符。

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

class MultiplyBy {
private:
    int factor;

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

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

然后,您可以将MultiplyBy对象传递给像std::transform这样的算法:

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,除非调用被内联化并且编译器知道您总是将同一个函数传递给它,否则它无法通过指针内联化调用。


1
我正好在寻找这个例子,因为我刚刚在一门C++课程中看到它,但我并没有理解它。通常,我们定义一个实现了operator()的类的对象,并将其作为参数(即函数对象)传递给transform等函数。然而,在这种情况下,我们只是在同一调用中构造对象。这是唯一的区别吗?也就是说,一旦transform完成,函数对象就会超出范围并被销毁吗?谢谢! - rturrado

46

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

一个函数对象是一个类或结构体对象,可以像函数一样被“调用”。这是通过重载() 运算符实现的。() 运算符(不确定它的名称)可以使用任意数量的参数。其他运算符只能取两个值,例如+ 运算符只能取两个值,并返回您为其重载的任何值。你可以在() 运算符中放置任意数量的参数,这就赋予了它灵活性。

要创建一个函数对象,首先创建你的类。然后为该类创建一个构造函数,带有您选择的类型和名称的参数。接下来,在同一语句中使用初始化器列表(其中使用单冒号运算符,这也是我新学习的内容),构造具有先前声明的构造函数参数的类成员对象。然后重载() 运算符。最后声明所创建的类或结构体的私有对象。

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

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.
}; 

如果有任何不准确或明显错误的地方,请随意纠正我!


1
() 运算符被称为函数调用运算符。我想你也可以称它为括号运算符。 - Gautam
4
这个参数实际上是我们刚刚编写的构造函数传递的参数“parameterVar”。 - Lightness Races in Orbit

26
一个functor是一个高阶函数,它将函数应用于参数化的(即模板化的)类型。它是map高阶函数的一般化。例如,我们可以像这样为std::vector定义一个functor:
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;
}

该函数接受一个std::vector<T>并在给定一个接受T并返回U的函数F时返回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 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)) 相同:
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));

2
本文主张应该正确地使用“functor”这个术语来表示其真正的含义(参见https://en.wikipedia.org/wiki/Functor),而将其用于函数对象只是一种懒散的做法。文章链接为:http://jackieokay.com/2017/01/26/functors.html。然而,考虑到这里有很多回答仅涉及函数对象的含义,可能已经为时已晚了。 - armb
4
这个答案应该是获得700个以上点赞的那一个。作为一个比起C++更了解Haskell的人,C++的术语常常让我感到困惑。 - mschmidt
1
范畴论和C++?这是Bartosz Milewski的秘密SO账户吗? - Mateen Ulhaq
1
在标准符号中总结函数子律可能会有所帮助:fmap(id, x) = id(x)fmap(f ◦ g, x) = fmap(f, fmap(g, x)) - Mateen Ulhaq
1
C++标准中没有提到函数对象(functor)。cppreference.com没有对函数对象(functor)进行定义,但提供了FunctionObject的定义,其中并未提及函数对象(functor)。 - Paul Fultz II
显示剩余2条评论

9
这是一个实际的情况,我被迫使用Functor解决我的问题:
我有一组函数(比如说20个),它们都是相同的,只是在3个特定位置调用不同的函数。
这是一种令人难以置信的浪费和代码重复。通常我会只传递一个函数指针,然后在这三个地方调用它。(这样代码只需要出现一次,而不是20次。)
但是我意识到,在每种情况下,特定的函数需要完全不同的参数配置!有时候是2个参数,有时候是5个参数,等等。
另一个解决方案是拥有一个基类,在派生类中具体的函数是一个重载方法。但是,我真的想要构建所有这些继承,只是为了传递一个函数指针吗?
解决方案:所以我做的是,我创建了一个包装器类(一个“Functor”),它能够调用我需要调用的任何函数。我提前设置好它(带有其参数等),然后我传递它而不是函数指针。现在,被调用的代码可以触发Functor,而不知道内部正在发生什么。它甚至可以多次调用它(我需要它调用3次)。
就是这样——一个实际的例子,其中Functor成为明显和容易的解决方案,使我将20个函数的代码重复减少到了1个。

3
如果你的函数对象调用不同的具体函数,并且这些其他函数接受的参数数量不同,这是否意味着你的函数对象接受可变数量的参数以分派到这些其他函数? - johnbakers
6
请引用部分代码来解释上述情况,我是C++的新手,想要理解这个概念。 - sanjeev

4

就像一再强调的那样,函数对象是可以被视为函数的类(重载运算符())。

它们最有用的情况是在需要将一些数据与重复或延迟调用函数相关联的情况下使用。

例如,函数对象的链表可用于实现基本低开销的同步协程系统、任务分发器或可中断的文件解析。

示例:

/* 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提供的内容不那么有用。

3
除了用于回调之外,C++函数对象还可以帮助提供类似Matlab的访问方式来访问矩阵类。这里有一个示例

这个(矩阵示例)只是简单地使用operator(),但没有利用函数对象的属性。 - renardesque

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接