为什么C++函数对象比具有命名方法的对象更可取?

7
我最近对函数对象产生了浓厚的兴趣,并在很多地方使用它们。但是,有一个情况让我需要让我的函数对象执行两个不同的操作,于是我考虑给我的函数对象添加另一个方法(而不是重载()运算符)。我不确定这是否是一种不好的做法(也许你可以告诉我),但这让我开始思考为什么我首先要使用函数对象而不是普通对象。所以我的问题是:
“重载()运算符是否有特殊之处,还是只是比使用常规命名方法略微更具语法吸引力?”
更新: 首先,我知道为什么函数对象可能比函数指针更可取,就像其他问题中解释的那样。我想知道为什么它们可能比具有命名方法的对象更可取。
其次,关于我想使用函数对象的另一个可能命名方法的示例:基本上我有两个函数,一个计算图分区的模块性 - compute_modularity(),另一个计算分区变化后模块性增益的compute_modularity_gain()。我认为我可以将这些函数作为同一个函数对象的一部分传递到优化算法中,增益函数为命名函数。我之所以不只是将两个函数对象传递到算法中,是因为我想强制实施compute_modularity_gain()仅与compute_modularity()一起使用,而不是与另一个函数对象(例如compute_stability())一起使用(后者应仅与compute_stability_gain()一起使用)。换句话说,增益函数必须与其兄弟函数紧密耦合。如果有其他方法可以强制执行此约束,请告诉我。

请描述让您想要添加另一个方法的情况。 - Marcelo Cantos
4
看起来这个问题已经得到解答了:C++ 函数对象及其用法 - chrisaycock
4
你只应该投票关闭完全相同的重复问题。 这个问题与之类似,但具有质量上不同的方面:为什么无法使用普通的命名方法实现函子,而它们只能实现一个方法? - Marcelo Cantos
@chrisaycock 我看到了那个问题,但似乎并没有回答我的问题,除非我漏掉了什么。我理解为什么一个函数对象可能比函数指针更可取,但我想知道它为什么比普通对象使用普通方法优越,而不是运算符重载。 - zenna
@zenna:你永远无法强制执行这样的约束。没有任何东西可以阻止调用者创建并传递一个正确实现compute_modularity_gain()并在调用compute_modularity()时使兔子从你的屏幕上跳出来的类。你所能做的最好的事情就是让他们做错事情时看起来很奇怪。例如,接受模块化相关函数的成员函数可以被称为setModularityFunctors(functor1, functor2),因此如果将稳定性相关的functor传递给该成员函数,它会看起来完全不合适。 - Marcelo Cantos
3个回答

7
operator()的重载是为了使仿函数具有与函数指针相同的调用语义--实际上,如果您愿意,可以使用函数指针。
重载operator()而不是使用函数有几个原因--但最重要的原因是编译器很少优化使用函数指针时的间接函数调用,但它们几乎总是优化掉operator()调用--这就是为什么std::sort通常比std::qsort更快的原因。
有许多复杂的原因,但真正的问题在于大多数(没有?)编译器都实现了可能删除函数指针调用的优化,这在现代硬件上是昂贵的。

然后出现了这样一种情况,我需要我的仿函数执行两个不同的操作

那么它就不再是一个仿函数了。要么传递两个仿函数来完成您想要的操作,要么定义一个模板方法类。(您还可以使用mixin在C++中实现模板方法模式而无需运行时开销--但Wikipedia文章未涵盖此内容)(还要注意:与C++模板不同,尽管如果您采用AOP路线,则可以涉及C++模板)

1

Functor 的基本意图是将知道如何执行某种工作的代码与知道何时需要执行该工作的代码分离开来(经典示例是将 Functor 与 UI 按钮关联起来)。

Functor 模型的一个小好处是普通的函数指针已经是 Functor。不需要额外的工作来包装它们。我认为这是一个小好处,因为 a)函数指针比直接调用函数稍微低效一些,b)我发现我几乎总是需要绑定某种形式的状态到我要包装的任何东西上,即使只是成员函数的 this 指针。

一元接口的关键优势在于它作为 Functor 的生产者和消费者之间的共同语言。你可以定义 Functor 具有一个 invoke() 成员函数,但是其他人可能会决定标准化使用 do(),而另一些人则可能选择使用 call()。所有这些解决方案都涉及更多的打字。

此外,在单个“functor”上具有多个成员函数从未严格要求。如果某些代码需要调用多个不同的操作,您可以简单地传递多个 Functor。这提供了很好的灵活性,因为这些操作可能是耦合的,也可能是完全不相关的。

一个解耦的例子是需要等式比较器和哈希函数的哈希表。在这种情况下,这两个函数可能是不相关的:将类的operator==()包装为相等性,将自由函数包装为计算哈希。
一个耦合的例子是发出几个不同事件的UI组件。单个类可以响应所有事件,或者不同的类可以响应不同组的事件。函数对象使得选择任何一种模型都很容易,而要求一个“接口”来定义组件所有事件的回调则更加笨拙。如果一个单一对象想要以不同的方式处理来自两个组件的事件,那么函数对象也会使这变得更加容易,因为你可以给每个组件一个不同的函数对象包装成员函数集。
最后,将现有功能包装在函数对象中是众所周知并得到广泛支持的库(如boost.bind)的做法,而创建实现doX()doY()的一次性类则不是。此外,新标准添加了lambda表达式,大大简化了函数对象的创建。

0

关于函数对象的唯一特殊之处在于它们可以像函数一样使用。然而,函数对象也可以通过它们的构造函数注入信息。

您可能还想了解std::function(如果您的编译器尚不支持,则使用boost::function),它可用于适配具有匹配调用签名的任何类型的对象。

std::bind或boost::bind允许您将具体参数与函数参数相关联,这允许与通过函数对象的构造函数传递它们相同的效果。您甚至可以使用bind提供this指针以调用成员函数,使其可以像普通函数对象一样调用,而无需显式指定对象。


“可能还可以通过它们的构造函数注入信息” <-- 不是它们的复制构造函数。请记住,函数对象必须是纯函数 - 您不应该在STL函数对象中存储状态,因为STL可以任意地复制您的函数对象,并且可以随意多次复制。 - Billy ONeal
是的,我不知道怎么会在想要输入普通构造函数时打出了复制构造函数,已经修改了。;o - xDD

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