web-dev-qa-db-ja.com

関数ではなくファンクターを使用する理由

比較する

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

少なくとも4つの理由:

懸念の分離

特定の例では、ファンクターベースのアプローチには、反復計算ロジックを平均計算ロジックから分離するという利点があります。そのため、他の状況でファンクターを使用できます(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は、この点で特殊なケースです(例: 有効な標準C++ライブラリ-for_eachと変換 を参照)。

パフォーマンス

多くの場合、ファンクターはコンパイラーによってインライン化できます(結局、STLは一連のテンプレートです)。同じことは理論的には関数にも当てはまりますが、通常、コンパイラーは関数ポインターを介してインライン化しません。標準的な例は、std::sort vs qsort; STLバージョンは、比較述語自体が単純であると仮定すると、多くの場合5〜10倍高速です。

概要

もちろん、最初の3つを従来の関数とポインターでエミュレートすることもできますが、ファンクターを使用することで非常に簡単になります。

75

ファンクターの利点:

  • 関数とは異なり、Functorは状態を持つことができます。
  • ファンクターは、関数と比較してOOPパラダイムに適合します。
  • 関数ポインターとは異なり、ファンクターは多くの場合インライン化されます
  • Functorはvtableとランタイムのディスパッチを必要としないため、ほとんどの場合により効率的です。
9
Alok Save

std::for_eachは、簡単に標準アルゴリズムの中で最も気まぐれで有用性が低いです。これはループの単なる素敵なラッパーです。ただし、それにも利点があります。

CalculateAverageの最初のバージョンがどのように見えるかを検討してください。反復子をループし、各要素で処理を行います。そのループを間違って書くとどうなりますか?おっとっと;コンパイラまたはランタイムエラーがあります。 2番目のバージョンでは、このようなエラーが発生することはありません。はい、大量のコードではありませんが、なぜループをそれほど頻繁に記述する必要があるのでしょうか?なぜ一度だけですか?

ここで、realアルゴリズムを検討してください。実際に機能するもの。 std::sortを書きますか?またはstd::find?またはstd::nth_element?可能な限り最も効率的な方法でそれを実装する方法を知っていますか?これらの複雑なアルゴリズムを何回実装しますか?

読みやすさについては、見る人の目にあります。言ったように、std::for_eachはアルゴリズムの最初の選択肢ではありません(特に、C++ 0xの範囲ベースの構文)。しかし、実際のアルゴリズムについて話している場合、それらは非常に読みやすいです。 std::sortはリストをソートします。 std::nth_elementのようなより曖昧なもののいくつかはそれほど馴染みがありませんが、便利なC++リファレンスでいつでも調べることができます。

また、C++ 0xでLambdaを使用すると、std :: for_eachでも完全に読み取り可能です。

7
Nicol Bolas

•関数とは異なり、ファンクターは状態を持つことができます。

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

Operator()をconstにしたまま、メンバー変数を変更する3つの方法を考えることができます。しかし、最善の方法は何ですか?次の例をご覧ください。

#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
joe

最初のアプローチでは、コレクションで何かをしたいすべての関数で反復コードを複製する必要があります。 2番目のアプローチは、反復の詳細を隠します。

2
Vijay Mathew

異なるレベルの抽象化で関数を比較しています。

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

前者はより多くのことを知る必要がありますが、一度知ってしまえばより簡単でエラーの可能性が少なくなります。

また、2つの汎用コンポーネント(std::accumulateおよびstd::plus)。これは、より複雑なケースでもよく見られます。多くの場合、シンプルで汎用的なファンクター(または関数;単純な古い関数がファンクターとして機能する)を持ち、必要なアルゴリズムと単純に組み合わせることができます。

1
Jan Hudec

OOPはここのキーワードです。

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

4.1ファンクターとは何ですか?

ファンクターは、状態を持つ関数です。 C++では、状態を保存する1つ以上のプライベートメンバーと、関数を実行するオーバーロードされた演算子()を持つクラスとしてそれらを実現できます。ファンクターは、概念テンプレートとポリモーフィズムを使用して、CおよびC++関数ポインターをカプセル化できます。任意のクラスのメンバー関数へのポインターのリストを作成し、それらのクラスやインスタンスへのポインターの必要性を気にすることなく、同じインターフェースを介してそれらをすべて呼び出すことができます。すべての関数は、同じ戻り値型と呼び出しパラメータを持っている必要があります。ファンクターはクロージャーとしても知られています。ファンクターを使用してコールバックを実装することもできます。