为什么要使用函数对象而不是普通函数?

61

比较

double average = CalculateAverage(values.begin(), values.end());

使用

double average = std::for_each(values.begin(), values.end(), CalculateAverage());

使用函数对象相较于普通函数有什么优势呢?即使在实现之前,第一个选项不是更易于阅读吗?

假设函数对象定义如下:

class CalculateAverage
{
private:
   std::size_t num;
   double sum;
public:

   CalculateAverage() : num (0) , sum (0)
   {
   }

   void operator () (double elem) 
   {
      num++; 
      sum += elem;
   }

   operator double() const
   {
       return sum / num;
   }
};

2
在第一种情况下,你需要自己循环遍历数组。不是吗? - Eric Z
for_each真的会返回平均值吗?难道不需要使用accumulate吗?请参见http://www.sgi.com/tech/stl/accumulate.html。在这里,您的第二行将CalculateAvarage()()应用于序列的每个成员,因此您需要一些聪明的运行平均计算,以及一个可以在`for_each`之后查询的CalculateAverage实例。`for_each`将返回您的函数对象的副本。 - juanchopanza
2
Functors提供了更多的灵活性,但通常会使用略微更多的内存,使用起来更难以正确使用,并且会牺牲一些效率。每个对象的内存成本微不足道,但当它达到100%时(例如一个函数指针与双倍的内存量相比),并且您有大量对象时,它就会计算在内。 "正确使用"的成本包括functors可以自由复制,因此要共享状态必须使用内部指针和可能的动态分配。后者也是主要的效率成本。 - Cheers and hth. - Alf
@juanchopanza:可能OP假设隐式转换CalculateAverage为double类型。我不明白如何使用accumulate()计算平均值(而不是总和!)。我们需要除以元素的数量... accumulate()的BinaryOperation如何知道这个数字?如果BinaryOperation具有状态并且并行计算总和和元素数量(根本不使用第二个操作数),那么它真的比for_each更清晰吗? - user396672
@user396672:你可以这样使用accumulate,例如:struct Average { double total; uintmax_t count; Average() : total(0), count(0) {} Average operator+(double d) { total += d; count += 1; }; operator double() { return total / count; /* undefined if 0! */ }};。如果不需要使用二元运算符参数,则无需使用它,而且操作符不需要跟踪计数,累加器可以(也应该)这样做。我认为这与传递给for_each的函数对象一样清晰,不同之处在于你要实现operator+而不是operator() - Steve Jessop
@Steve Jessop: 起初,我认为累加器方法更加复杂和不太清晰,但是我找到了一个论据支持累加器。如果累加器的二元运算具有幺半群性质(在计算平均值的情况下),集合上的操作可以任意分组并且(理论上)可以并行运行(我明白当前 STL 算法的实现和迭代器概念本身本质上是顺序的,但是累加器方法确实似乎更“函数式”和“声明式”)。 - user396672
7个回答

85

至少有四个好的原因:

责任分离

在你的这个例子中,基于函数对象的方法有一个优点,即将迭代逻辑与平均值计算逻辑分离开来。所以你可以在其他情况下使用你的函数对象(想一想STL中的所有其他算法),并且你可以使用其他函数对象与for_each一起使用。

参数化

你可以更容易地对函数对象进行参数化。例如,你可以有一个CalculateAverageOfPowers函数对象,它采用你的数据的平方、立方等的平均值,编写如下:

class CalculateAverageOfPowers
{
public:
    CalculateAverageOfPowers(float p) : acc(0), n(0), p(p) {}
    void operator() (float x) { acc += pow(x, p); n++; }
    float getAverage() const { return acc / n; }
private:
    float acc;
    int   n;
    float p;
};

当然,您也可以使用传统函数来完成同样的事情,但这样会使其难以与函数指针一起使用,因为它具有不同于CalculateAverage的原型。

状态性

而且,由于函数对象可以是有状态的,所以您可以像这样做:

CalculateAverage avg;
avg = std::for_each(dataA.begin(), dataA.end(), avg);
avg = std::for_each(dataB.begin(), dataB.end(), avg);
avg = std::for_each(dataC.begin(), dataC.end(), avg);

对多个不同数据集进行平均的方法。

请注意,几乎所有接受函数对象的STL算法/容器都要求它们是“纯”谓词,即在时间上没有可观察的状态改变。在这方面,for_each是一个特殊情况(例如参见Effective Standard C++ Library - for_each vs. transform)。

性能

编译器通常可以内联函数对象(毕竟,STL是一堆模板)。虽然理论上函数也可以内联,但编译器通常不会通过函数指针进行内联。经典例子是比较std::sortqsort;假设比较谓词本身很简单,STL版本通常快5-10倍。

总结

当然,使用传统函数和指针可以模拟前三者,但使用函数对象变得更加简单。


欢迎您,但我的意思是给您一个更重要的提示,这将从根本上影响您的答案(我认为)。我刚试了一下您的代码,Visual C++ 报错 operator =' function is unavailable in 'CalculateAverageOfPowers'。g++ 同样抱怨 error: non-static const member 'const float CalculateAverageOfPowers::p', can't use default assignment operator - Cheers and hth. - Alf
@Oli:上次我测试qsort时,它分配了额外的内存,因此这相当影响性能。此外,它需要一个指针表,因此局部性较低...最好比较带谓词的std::sort和带函数指针的std::sort,以阐明你的观点。 - Matthieu M.
1
@Matthieu:这是一个公正的观点。老实说,我在回应Scott Meyers在《Effective STL》中提出的观点,也是我在实践中观察到的。我会尝试使用函数指针和函数对象对std::sort进行性能分析。但从根本上讲,我认为qsort也可以像std::sort一样使用函数指针来实现。 - Oliver Charlesworth
1
@Matthieu:“上次我测试qsort时,它分配了额外的内存”- 我很惊讶,我期望在qsort中有足够的空间在堆栈上,并且几乎任何qsort实现都依赖于此。此外,malloc允许失败而qsort不允许,因此通常需要作为特殊情况中止。最后,qsort不需要指针表,元素大小是其参数之一,并将指向元素的指针传递给比较器函数。您可以对任何数组进行qsort并期望与其他类似数组操作具有相同的局部性。 - Steve Jessop
1
状态示例非常好,我从未想过使用类似于函数对象的东西。 - DanDan
显示剩余15条评论

10

Functor的优点:

  • 与函数不同,Functor可以拥有状态。
  • 与函数相比,Functor更适合OOP范例。
  • 与函数指针不同,Functor通常可以内联。
  • Functor不需要虚表和运行时分派,因此在大多数情况下更有效率。

9

std::for_each可能是标准算法中最任性且最无用的一个。它只是一个循环的美妙封装。然而,即使如此,它也有优点。

考虑一下你的第一个版本的CalculateAverage应该是什么样子的。它将在迭代器上循环,然后对每个元素执行操作。如果您编写的循环不正确会发生什么?哎呀;就会出现编译器或运行时错误。第二个版本永远不会出现这样的错误。是的,这不是很多代码,但为什么我们经常要编写循环呢?为什么不只写一次呢?

现在,考虑一下真正的算法;那些实际工作的算法。你想写std::sort吗?还是std::find?或者std::nth_element?你甚至知道如何以最有效的方式实现它吗?你想要多少次实现这些复杂的算法呢?

至于易读性,这取决于观察者的角度。正如我所说,std::for_each几乎不是算法的首选(特别是使用 C++0x 的基于范围的 for 语法)。但是如果你谈论真正的算法,它们非常易读;std::sort对列表进行排序。一些更为晦涩的算法(如std::nth_element)可能不那么熟悉,但是您可以在方便的C++参考书中查找。

而且,一旦您在C++0x中使用Lambda,甚至std::for_each也是完全可读的。


3

•与函数不同,函数对象可以拥有状态。

这非常有趣,因为std::binary_function、std::less和std::equal_to都有一个const的operator()模板。但是,如果您想要打印具有该对象当前调用计数的调试消息,该怎么做呢?

这是std::equal_to的模板:

struct equal_to : public binary_function<_Tp, _Tp, bool>
{
  bool
  operator()(const _Tp& __x, const _Tp& __y) const
  { return __x == __y; }
};

我能想到3种方法来允许operator()保持const,同时改变成员变量。但哪种方法最好呢?以这个例子为例:

#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
#include <cassert>  // assert() MACRO

// functor for comparing two integer's, the quotient when integer division by 10.
// So 50..59 are same, and 60..69 are same.
// Used by std::sort()

struct lessThanByTen: public std::less<int>
{
private:
    // data members
    int count;  // nr of times operator() was called

public:
    // default CTOR sets count to 0
    lessThanByTen() :
        count(0)
    {
    }


    // @override the bool operator() in std::less<int> which simply compares two integers
    bool operator() ( const int& arg1, const int& arg2) const
    {
        // this won't compile, because a const method cannot change a member variable (count)
//      ++count;


        // Solution 1. this trick allows the const method to change a member variable
        ++(*(int*)&count);

        // Solution 2. this trick also fools the compilers, but is a lot uglier to decipher
        ++(*(const_cast<int*>(&count)));

        // Solution 3. a third way to do same thing:
        {
        // first, stack copy gets bumped count member variable
        int incCount = count+1;

        const int *iptr = &count;

        // this is now the same as ++count
        *(const_cast<int*>(iptr)) = incCount;
        }

        std::cout << "DEBUG: operator() called " << count << " times.\n";

        return (arg1/10) < (arg2/10);
    }
};

void test1();
void printArray( const std::string msg, const int nums[], const size_t ASIZE);

int main()
{
    test1();
    return 0;
}

void test1()
{
    // unsorted numbers
    int inums[] = {33, 20, 10, 21, 30, 31, 32, 22, };

    printArray( "BEFORE SORT", inums, 8 );

    // sort by quotient of integer division by 10
    std::sort( inums, inums+8, lessThanByTen() );

    printArray( "AFTER  SORT", inums, 8 );

}

//! @param msg can be "this is a const string" or a std::string because of implicit string(const char *) conversion.
//! print "msg: 1,2,3,...N", where 1..8 are numbers in nums[] array

void printArray( const std::string msg, const int nums[], const size_t ASIZE)
{
    std::cout << msg << ": ";
    for (size_t inx = 0; inx < ASIZE; ++inx)
    {
        if (inx > 0)
            std::cout << ",";
        std::cout << nums[inx];
    }
    std::cout << "\n";
}

因为这3个解决方案都是编译进去的,所以它将计数增加了3。以下是输出结果:

gcc -g -c Main9.cpp
gcc -g Main9.o -o Main9 -lstdc++
./Main9
BEFORE SORT: 33,20,10,21,30,31,32,22
DEBUG: operator() called 3 times.
DEBUG: operator() called 6 times.
DEBUG: operator() called 9 times.
DEBUG: operator() called 12 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 12 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 18 times.
DEBUG: operator() called 18 times.
DEBUG: operator() called 21 times.
DEBUG: operator() called 21 times.
DEBUG: operator() called 24 times.
DEBUG: operator() called 27 times.
DEBUG: operator() called 30 times.
DEBUG: operator() called 33 times.
DEBUG: operator() called 36 times.
AFTER  SORT: 10,20,21,22,33,30,31,32

2
第四个解决方案是使计数可变。 - DanDan

2
在第一种方法中,迭代代码必须在所有想要对集合执行某些操作的函数中进行复制。第二种方法隐藏了迭代的细节。

1

你正在比较不同抽象级别的函数。

你可以将CalculateAverage(begin, end)实现为以下任一形式:

template<typename Iter>
double CalculateAverage(Iter begin, Iter end)
{
    return std::accumulate(begin, end, 0.0, std::plus<double>) / std::distance(begin, end)
}

或者你可以使用 for 循环来实现

template<typename Iter>
double CalculateAverage(Iter begin, Iter end)
{
    double sum = 0;
    int count = 0;
    for(; begin != end; ++begin) {
        sum += *begin;
        ++count;
    }
    return sum / count;
}

前者要求你知道更多的内容,但一旦你知道了它们,就更简单,并且留下了较少的错误可能性。

它还仅使用了两个通用组件 (std::accumulatestd::plus) ,这在更复杂的情况下通常也是如此。您通常可以拥有一个简单的通用函数对象(或函数;普通旧函数可作为函数对象)并将其与您需要的任何算法组合即可。


1

OOP是关键词。

http://www.newty.de/fpt/functor.html:

4.1 什么是函数对象?

函数对象是带有状态的函数。在C++中,您可以将它们实现为一个类,该类具有一个或多个私有成员来存储状态,并具有重载运算符()以执行函数。函数对象可以封装C和C++函数指针,采用模板和多态性概念。您可以建立一个任意类的成员函数指针列表,并通过相同的接口调用它们,而不必担心它们的类或需要指向实例的指针。所有函数只需具有相同的返回类型和调用参数即可。有时函数对象也称为闭包。您还可以使用函数对象来实现回调。


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