1052 Stimmen

Was sind C++-Funktoren und ihre Verwendung?

Ich höre immer wieder viel über Funktoren in C++. Kann mir jemand einen Überblick darüber geben, was sie sind und in welchen Fällen sie nützlich sein könnten?

26voto

Paul Fultz II Punkte 16880

Ein Funktor ist ein übergeordnete Funktion die eine Funktion auf die parametrisierten (d.h. schablonierten) Typen anwendet. Es ist eine Verallgemeinerung der Karte Funktion höherer Ordnung. Wir könnten zum Beispiel einen Funktor definieren für std::vector wie diese:

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

Diese Funktion nimmt eine std::vector<T> und gibt zurück std::vector<U> wenn eine Funktion gegeben ist F die eine T und gibt eine U . Ein Funktor muss nicht über Containertypen definiert werden, er kann auch für jeden beliebigen Schablonentyp definiert werden, einschließlich 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)));
}

Hier ein einfaches Beispiel, das den Typ in eine 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);

Es gibt zwei Gesetze, denen Funktoren folgen sollten. Das erste ist das Identitätsgesetz, das besagt, dass, wenn dem Funktor eine Identitätsfunktion gegeben wird, er dasselbe sein sollte wie die Anwendung der Identitätsfunktion auf den Typ, d. h. fmap(identity, x) sollte dasselbe sein wie 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);

Das nächste Gesetz ist das Kompositionsgesetz, das besagt, dass, wenn der Funktor eine Komposition zweier Funktionen ist, es dasselbe sein sollte wie die Anwendung des Funktors für die erste Funktion und dann wieder für die zweite Funktion. Also, fmap(std::bind(f, std::bind(g, _1)), x) sollte dasselbe sein wie 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));

9voto

Fellow Traveler Punkte 324

Hier ist eine aktuelle Situation, in der ich gezwungen war, einen Functor zu verwenden, um mein Problem zu lösen:

Ich habe eine Reihe von Funktionen (sagen wir 20), die alle identisch sind, außer dass jede eine andere spezifische Funktion an 3 spezifischen Stellen aufruft.

Das ist eine unglaubliche Verschwendung und Code-Duplizierung. Normalerweise würde ich einfach einen Funktionszeiger übergeben und diesen an den 3 Stellen aufrufen. (So muss der Code nur einmal statt zwanzig Mal erscheinen.)

Aber dann wurde mir klar, dass die jeweilige Funktion ein völlig anderes Parameterprofil erfordert! Manchmal 2 Parameter, manchmal 5 Parameter, usw.

Eine andere Lösung wäre, eine Basisklasse zu haben, in der die spezifische Funktion eine überschriebene Methode in einer abgeleiteten Klasse ist. Aber will ich wirklich diese ganze Vererbung aufbauen, nur damit ich einen Funktionszeiger übergeben kann????

LÖSUNG: Also, was ich tat, war, ich machte eine Wrapper-Klasse (ein "Functor"), die in der Lage ist, jede der Funktionen, die ich brauchte aufgerufen. Ich habe ihn im Voraus eingerichtet (mit seinen Parametern usw.) und ihn dann anstelle eines Funktionszeigers übergeben. Nun kann der aufgerufene Code den Functor auslösen, ohne zu wissen, was im Inneren passiert. Er kann ihn sogar mehrfach aufrufen (ich brauchte ihn für 3 Aufrufe).


Das war's - ein praktisches Beispiel, bei dem sich ein Functor als die offensichtliche und einfache Lösung herausstellte, die es mir ermöglichte, die Codeduplizierung von 20 Funktionen auf 1 zu reduzieren.

4voto

nfries88 Punkte 297

Wie bereits erwähnt, sind Funktoren Klassen, die wie Funktionen behandelt werden können (Überladungsoperator ()).

Sie sind besonders nützlich für Situationen, in denen Sie einige Daten mit wiederholten oder verzögerten Aufrufen einer Funktion verknüpfen müssen.

Beispielsweise könnte eine verknüpfte Liste von Funktoren verwendet werden, um ein einfaches synchrones Coroutine-System mit geringem Aufwand, einen Task-Dispatcher oder eine unterbrechbare Dateiparserfunktion zu implementieren. Beispiele:

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

Natürlich sind diese Beispiele für sich genommen nicht sehr nützlich. Sie zeigen nur, wie Funktoren nützlich sein können. Die Funktoren selbst sind sehr einfach und unflexibel, und das macht sie weniger nützlich als zum Beispiel das, was boost bietet.

3voto

erandros Punkte 1755

Funktoren werden in gtkmm verwendet, um eine GUI-Schaltfläche mit einer tatsächlichen C++-Funktion oder -Methode zu verbinden.


Wenn Sie die pthread-Bibliothek verwenden, um Ihre Anwendung multithreadingfähig zu machen, können Functors Ihnen helfen.
Um einen Thread zu starten, muss eines der Argumente des Befehls pthread_create(..) ist der Funktionszeiger, der auf seinem eigenen Thread ausgeführt wird.
Aber es gibt eine Unannehmlichkeit. Dieser Zeiger kann kein Zeiger auf eine Methode sein, es sei denn, er ist ein statische Methode oder wenn Sie seine Klasse angeben , wie class::method . Und noch etwas, die Schnittstelle Ihrer Methode kann nur sein:

void* method(void* something)

Sie können also nicht (auf einfache, offensichtliche Weise) Methoden Ihrer Klasse in einem Thread ausführen, ohne etwas Zusätzliches zu tun.

Eine sehr gute Möglichkeit, mit Threads in C++ umzugehen, ist die Erstellung einer eigenen Thread Klasse. Wenn Sie die Methoden von MyClass Klasse, habe ich diese Methoden in folgende Methoden umgewandelt Functor abgeleitete Klassen.

Auch die Thread Klasse hat diese Methode: static void* startThread(void* arg)
Ein Zeiger auf diese Methode wird als Argument verwendet, um die pthread_create(..) . Und was startThread(..) erhalten sollte, ist ein void* gecasteter Verweis auf eine Instanz im Heap eines beliebigen Functor abgeleitete Klasse, die zurück in Functor* ausgeführt wird, und ruft dann seine run() méthode.

3voto

Yantao Xie Punkte 11260

Abgesehen von der Verwendung in Callbacks können C++-Funktoren auch dabei helfen, eine Matlab wie der Zugriffsstil auf eine Matrix Klasse. Es gibt eine Beispiel .

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X