为什么在函数上使用函子?

时间:2011-06-23 09:21:23

标签: c++ stl functor

比较

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

7 个答案:

答案 0 :(得分:74)

至少有四个很好的理由:

关注点分离

在您的特定示例中,基于仿函数的方法具有将迭代逻辑与平均计算逻辑分离的优点。因此,您可以在其他情况下使用您的仿函数(考虑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倍。

<强>摘要

当然,可以使用传统函数和指针来模拟前三个,但是使用仿函数会变得更加简单。

答案 1 :(得分:9)

Functors的优点:

  • 与函数不同,Functor可以具有状态。
  • 与函数相比,Functor适用于OOP范例。
  • 与函数指针
  • 不同,Functor通常可以内联
  • Functor不需要vtable和运行时调度,因此在大多数情况下更有效。

答案 2 :(得分:7)

std::for_each很容易成为标准算法中反复无常且最不实用的。它只是一个很好的循环包装器。然而,即使它有优势。

考虑您的CalculateAverage的第一个版本必须是什么样子。它将在迭代器上有一个循环,然后对每个元素进行处理。如果你错误地写了这个循环怎么办?哎呀;有编译器或运行时错误。第二个版本永远不会有这样的错误。是的,这不是很多代码,但为什么我们必须经常编写循环?为什么不一次?

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

至于阅读的方便性,这是在旁观者的眼中。正如我所说,std::for_each几乎不是算法的首选(尤其是C ++ 0x基于范围的语法)。但是,如果你在谈论真正的算法,它们是非常可读的; std::sort对列表进行排序。像std::nth_element这样的一些比较模糊的东西不会那么熟悉,但是你总是可以在你方便的C ++参考中查找它。

一旦你在C ++ 0x中使用Lambda,即使是std :: for_each也是完全可读的。

答案 3 :(得分:2)

在第一种方法中,迭代代码必须在所有想要对集合做某事的函数中重复。第二种方法隐藏了迭代的细节。

答案 4 :(得分:2)

•与功能不同,Functor可以具有状态。

这非常有趣,因为std :: binary_function,std :: less和std :: equal_to有一个const()运算符的模板。但是如果你想用这个对象的当前调用计数打印调试消息怎么办呢?

这是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

答案 5 :(得分:1)

OOP是关键字。

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

4.1什么是Functors?

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

答案 6 :(得分: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),在更复杂的情况下也是如此。你通常可以拥有一个简单的,通用的函子(或函数;普通的旧函数可以作为函子),并简单地将它与你需要的任何算法结合起来。