簡體   English   中英

什么是 C++ 函子及其用途?

[英]What are C++ functors and their uses?

我一直聽到很多關於 C++ 中的函子的消息。 有人能給我一個關於它們是什么以及它們在什么情況下有用的概述嗎?

函子幾乎只是一個定義 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

函子有一些不錯的地方。 一是與常規函數不同,它們可以包含狀態。 上面的例子創建了一個函數,它把你給的任何東西都加上 42。 但是值 42 不是硬編碼的,它在我們創建函子實例時被指定為構造函數參數。 我可以創建另一個加法器,它添加了 27,只需調用具有不同值的構造函數即可。 這使得它們可以很好地定制。

如最后幾行所示,您經常將函子作為參數傳遞給其他函數,例如 std::transform 或其他標准庫算法。 您可以對常規函數指針執行相同操作,但正如我上面所說,函子可以“定制”,因為它們包含狀態,從而使它們更加靈活(如果我想使用函數指針,我必須編寫一個函數它在它的參數中正好加了 1。函子是通用的,並添加了你初始化它的任何東西),而且它們也可能更有效。 在上面的例子中,編譯器確切地知道std::transform應該調用哪個函數。 它應該調用add_x::operator() 這意味着它可以內聯該函數調用。 這使得它就像我在向量的每個值上手動調用函數一樣高效。

如果我傳遞了一個函數指針,編譯器將無法立即看到它指向哪個函數,因此除非它執行一些相當復雜的全局優化,否則它必須在運行時取消引用該指針,然后進行調用。

小補充。 您可以使用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));

所有這些東西都有一個問題,編譯器錯誤消息不是人類可讀的:)

Functor 是一個像函數一樣的對象。 基本上,一個定義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)
{ ....}

早在 C++ 出現之前,“函子”這個名稱就已經被傳統地用於范疇論中。 這與函子的 C++ 概念無關。 最好使用名稱函數對象而不是我們在 C++ 中所說的“函子”。 這就是其他編程語言如何調用類似結構。

用於代替普通函數:

特征:

  • 函數對象可能有狀態
  • 函數對象適合 OOP(它的行為與其他所有對象一樣)。

缺點:

  • 給程序帶來了更多的復雜性。

用於代替函數指針:

特征:

  • 函數對象通常可以被內聯

缺點:

  • 函數對象在運行時不能與其他函數對象類型交換(至少除非它擴展了某個基類,因此會產生一些開銷)

代替虛函數使用:

特征:

  • 函數對象(非虛擬)不需要 vtable 和運行時分派,因此在大多數情況下效率更高

缺點:

  • 函數對象在運行時不能與其他函數對象類型交換(至少除非它擴展了某個基類,因此會產生一些開銷)

就像其他人提到的那樣,函子是一個對象,其行為類似於函數,即它重載了函數調用運算符。

函子通常用於 STL 算法。 它們很有用,因為它們可以在函數調用之前和之間保持狀態,就像函數式語言中的閉包一樣。 例如,您可以定義一個MultiplyBy函子,將其參數乘以指定的數量:

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 ,除非調用被內聯並且編譯器知道您總是將相同的函數傳遞給它,否則它無法通過指針內聯調用。

對於我們中間像我這樣的新手:經過一些研究,我弄清楚了 jalf 發布的代碼做了什么。

函子是一個類或結構對象,可以像函數一樣被“調用”。 這是通過重載() operator () operator (不確定它叫什么)可以接受任意數量的參數。 其他運算符只取兩個,即+ operator只能取兩個值(運算符的每一側一個)並返回您為其重載的任何值。 您可以在() operator放入任意數量的參數,這為其提供了靈活性。

要首先創建函子,您需要創建您的類。 然后使用您選擇的類型和名稱的參數為類創建一個構造函數。 在同一個語句中,后面跟着一個初始化列表(它使用單個冒號運算符,我也是新手),它使用先前聲明的構造函數參數構造類成員對象。 然后() operator被重載。 最后聲明您創建的類或結構的私有對象。

我的代碼(我發現 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.
}; 

如果其中任何一個不准確或完全錯誤,請隨時糾正我!

函子是將函數應用於參數化(即模板化)類型的高階函數 它是map高階函數的推廣。 例如,我們可以像這樣為std::vector定義一個函子:

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

當給定一個接受T並返回U的函數F ,該函數接受一個std::vector<T>並返回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));

這是我被迫使用 Functor 來解決我的問題的實際情況:

我有一組函數(比如其中的 20 個),它們都是相同的,除了每個函數在 3 個特定位置調用不同的特定函數。

這是令人難以置信的浪費和代碼重復。 通常我只會傳入一個函數指針,然后在 3 個位置調用它。 (所以代碼只需要出現一次,而不是二十次。)

但后來我意識到,在每種情況下,特定功能都需要完全不同的參數配置文件! 有時是 2 個參數,有時是 5 個參數,等等。

另一種解決方案是擁有一個基類,其中特定函數是派生類中的重寫方法。 但是我真的想構建所有這些繼承,只是為了傳遞一個函數指針????

解決方案:所以我所做的是,我創建了一個包裝類(一個“函數”),它能夠調用我需要調用的任何函數。 我提前設置了它(使用它的參數等),然后我傳遞它而不是函數指針。 現在被調用的代碼可以觸發 Functor,而無需知道內部發生了什么。 它甚至可以多次調用它(我需要它調用 3 次。)


就是這樣——一個實際的例子,一個 Functor 被證明是顯而易見的和簡單的解決方案,它使我能夠將代碼重復從 20 個函數減少到 1 個。

就像已經重復過的那樣,函子是可以被視為函數的類(重載運算符 ())。

在需要將某些數據與對函數的重復或延遲調用相關聯的情況下,它們最有用。

例如,函子的鏈表可用於實現基本的低開銷同步協程系統、任務調度程序或可中斷文件解析。 例子:

/* 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 提供的有用。

在 gtkmm 中使用函子將一些 GUI 按鈕連接到實際的 C++ 函數或方法。


如果您使用 pthread 庫使您的應用程序多線程,Functors 可以幫助您。
要啟動一個線程, pthread_create(..)的參數之一是要在他自己的線程上執行的函數指針。
但是有一個不便。 這個指針不能是指向方法的指針,除非它是一個靜態方法,或者除非你指定它是 class ,比如class::method 還有一件事,你的方法的接口只能是:

void* method(void* something)

所以你不能在一個線程中運行(以一種簡單明顯的方式)你的類中的方法而不做一些額外的事情。

在 C++ 中處理線程的一個很好的方法是創建自己的Thread類。 如果你想從MyClass類運行方法,我所做的是將這些方法轉換為Functor派生類。

另外, Thread類有這個方法: static void* startThread(void* arg)
指向此方法的指針將用作調用pthread_create(..)的參數。 startThread(..)應該在 arg 中接收到一個void*對任何Functor派生類的堆中實例的引用,該實例在執行時將被強制轉換回Functor* ,然后調用它的run()方法。

除了用於回調之外,C++ 函子還可以幫助為矩陣類提供Matlab喜歡的訪問方式。 有一個例子

另外,我使用函數對象將現有的遺留方法適合命令模式; (唯一能感受到 OO 范式之美的真正 OCP 的地方); 還在這里添加了相關的函數適配器模式。

假設您的方法具有以下簽名:

int CTask::ThreeParameterTask(int par1, int par2, int par3)

我們將看到如何將它適合命令模式 - 為此,首先,您必須編寫一個成員函數適配器,以便可以將其作為函數對象調用。

注意 - 這很難看,可能你可以使用 Boost 綁定助手等,但如果你不能或不想,這是一種方法。

// a template class for converting a member function of the type int        function(int,int,int)
//to be called as a function object
template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>
class mem_fun3_t
{
  public:
explicit mem_fun3_t(_Ret (_Class::*_Pm)(_arg1,_arg2,_arg3))
    :m_Ptr(_Pm) //okay here we store the member function pointer for later use
    {}

//this operator call comes from the bind method
_Ret operator()(_Class *_P, _arg1 arg1, _arg2 arg2, _arg3 arg3) const
{
    return ((_P->*m_Ptr)(arg1,arg2,arg3));
}
private:
_Ret (_Class::*m_Ptr)(_arg1,_arg2,_arg3);// method pointer signature
};

此外,我們需要上述類的輔助方法mem_fun3來幫助調用。

template<typename _Ret,typename _Class,typename _arg1,typename _arg2,typename _arg3>
mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3> mem_fun3 ( _Ret (_Class::*_Pm)          (_arg1,_arg2,_arg3) )
{
  return (mem_fun3_t<_Ret,_Class,_arg1,_arg2,_arg3>(_Pm));
}

現在,為了綁定參數,我們必須編寫一個綁定函數。 所以,這里是:

template<typename _Func,typename _Ptr,typename _arg1,typename _arg2,typename _arg3>
class binder3
{
public:
//This is the constructor that does the binding part
binder3(_Func fn,_Ptr ptr,_arg1 i,_arg2 j,_arg3 k)
    :m_ptr(ptr),m_fn(fn),m1(i),m2(j),m3(k){}

 //and this is the function object 
 void operator()() const
 {
        m_fn(m_ptr,m1,m2,m3);//that calls the operator
    }
private:
    _Ptr m_ptr;
    _Func m_fn;
    _arg1 m1; _arg2 m2; _arg3 m3;
};

並且,一個使用 binder3 類的輔助函數 - bind3

//a helper function to call binder3
template <typename _Func, typename _P1,typename _arg1,typename _arg2,typename _arg3>
binder3<_Func, _P1, _arg1, _arg2, _arg3> bind3(_Func func, _P1 p1,_arg1 i,_arg2 j,_arg3 k)
{
    return binder3<_Func, _P1, _arg1, _arg2, _arg3> (func, p1,i,j,k);
}

現在,我們必須在 Command 類中使用它; 使用以下類型定義:

typedef binder3<mem_fun3_t<int,T,int,int,int> ,T* ,int,int,int> F3;
//and change the signature of the ctor
//just to illustrate the usage with a method signature taking more than one parameter
explicit Command(T* pObj,F3* p_method,long timeout,const char* key,
long priority = PRIO_NORMAL ):
m_objptr(pObj),m_timeout(timeout),m_key(key),m_value(priority),method1(0),method0(0),
method(0)
{
    method3 = p_method;
}

這是你如何稱呼它的:

F3 f3 = PluginThreadPool::bind3( PluginThreadPool::mem_fun3( 
      &CTask::ThreeParameterTask), task1,2122,23 );

注意: f3(); 將調用方法task1->ThreeParameterTask(21,22,23); .

此模式的完整上下文位於以下鏈接

將函數實現為函子的一大優勢是它們可以在調用之間維護和重用狀態。 例如,許多動態規划算法(如用於計算字符串之間的Levenshtein 距離Wagner-Fischer 算法)通過填充一個大的結果表來工作。 每次調用函數時都分配這張表是非常低效的,因此將函數實現為函子並將該表作為成員變量可以大大提高性能。

下面是將 Wagner-Fischer 算法實現為函子的示例。 注意表是如何在構造函數中分配的,然后在operator()重用,並根據需要調整大小。

#include <string>
#include <vector>
#include <algorithm>

template <typename T>
T min3(const T& a, const T& b, const T& c)
{
   return std::min(std::min(a, b), c);
}

class levenshtein_distance 
{
    mutable std::vector<std::vector<unsigned int> > matrix_;

public:
    explicit levenshtein_distance(size_t initial_size = 8)
        : matrix_(initial_size, std::vector<unsigned int>(initial_size))
    {
    }

    unsigned int operator()(const std::string& s, const std::string& t) const
    {
        const size_t m = s.size();
        const size_t n = t.size();
        // The distance between a string and the empty string is the string's length
        if (m == 0) {
            return n;
        }
        if (n == 0) {
            return m;
        }
        // Size the matrix as necessary
        if (matrix_.size() < m + 1) {
            matrix_.resize(m + 1, matrix_[0]);
        }
        if (matrix_[0].size() < n + 1) {
            for (auto& mat : matrix_) {
                mat.resize(n + 1);
            }
        }
        // The top row and left column are prefixes that can be reached by
        // insertions and deletions alone
        unsigned int i, j;
        for (i = 1;  i <= m; ++i) {
            matrix_[i][0] = i;
        }
        for (j = 1; j <= n; ++j) {
            matrix_[0][j] = j;
        }
        // Fill in the rest of the matrix
        for (j = 1; j <= n; ++j) {
            for (i = 1; i <= m; ++i) {
                unsigned int substitution_cost = s[i - 1] == t[j - 1] ? 0 : 1;
                matrix_[i][j] =
                    min3(matrix_[i - 1][j] + 1,                 // Deletion
                    matrix_[i][j - 1] + 1,                      // Insertion
                    matrix_[i - 1][j - 1] + substitution_cost); // Substitution
            }
        }
        return matrix_[m][n];
    }
};

Functor 也可用於模擬在函數內定義局部函數。 請參閱問題另一個.

但是本地函子不能訪問外部自動變量。 lambda (C++11) 函數是更好的解決方案。

我已經“發現”了一個非常有趣的函子用法:當我對一個方法沒有很好的名字時,我就使用它們,因為函子是沒有名稱的方法;-)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM