Was ist ein C ++ - Delegat?

147

Was ist die allgemeine Idee eines Delegaten in C ++? Was sind sie, wie werden sie verwendet und wofür werden sie verwendet?

Ich würde sie gerne zuerst auf eine Art „Black Box“ kennenlernen, aber ein bisschen Information über die Eingeweide dieser Dinge wäre auch großartig.

Dies ist nicht C ++ in seiner reinsten oder saubersten Form, aber ich stelle fest, dass die Codebasis, in der ich arbeite, sie im Überfluss enthält. Ich hoffe, sie genug zu verstehen, damit ich sie einfach verwenden kann und nicht in die schreckliche Schrecklichkeit verschachtelter Vorlagen eintauchen muss.

Diese beiden Artikel von The Code Project erklären, was ich meine, aber nicht besonders prägnant:

Sir Yakalot
quelle
4
Sprechen Sie über verwaltetes C ++ unter .NET?
Dasblinkenlight
2
Haben Sie sich die Wikipedia-Seite für Delegation angesehen ?
Matthijs Bierman
5
delegateist im C ++ - Sprachgebrauch kein gebräuchlicher Name. Sie sollten der Frage einige Informationen hinzufügen, um den Kontext einzuschließen, in dem Sie sie gelesen haben. Beachten Sie, dass das Muster zwar häufig vorkommt, die Antworten jedoch unterschiedlich sein können, wenn Sie über Delegate im Allgemeinen oder im Kontext von C ++ CLI oder einer anderen Bibliothek mit einer bestimmten Implementierung von Delegate sprechen .
David Rodríguez - Dribeas
6
2 Jahre warten ?;)
Rang 1
6
Ich

Antworten:

173

Sie haben eine unglaubliche Anzahl von Möglichkeiten, um Delegierte in C ++ zu erreichen. Hier sind diejenigen, die mir in den Sinn gekommen sind.


Option 1: Funktoren:

Ein Funktionsobjekt kann durch Implementierung erstellt werden operator()

struct Functor
{
     // Normal class/struct members

     int operator()(double d) // Arbitrary return types and parameter list
     {
          return (int) d + 1;
     }
};

// Use:
Functor f;
int i = f(3.14);

Option 2: Lambda-Ausdrücke (nur C ++ 11 )

// Syntax is roughly: [capture](parameter list) -> return type {block}
// Some shortcuts exist
auto func = [](int i) -> double { return 2*i/1.15; };
double d = func(1);

Option 3: Funktionszeiger

int f(double d) { ... }
typedef int (*MyFuncT) (double d);
MyFuncT fp = &f;
int a = fp(3.14);

Option 4: Zeiger auf Mitgliedsfunktionen (schnellste Lösung)

Siehe Fast C ++ Delegate (im Code-Projekt ).

struct DelegateList
{
     int f1(double d) { }
     int f2(double d) { }
};

typedef int (DelegateList::* DelegateType)(double d);

DelegateType d = &DelegateList::f1;
DelegateList list;
int a = (list.*d)(3.14);

Option 5: std :: function

(oder boost::functionwenn Ihre Standardbibliothek dies nicht unterstützt). Es ist langsamer, aber am flexibelsten.

#include <functional>
std::function<int(double)> f = [can be set to about anything in this answer]
// Usually more useful as a parameter to another functions

Option 6: Bindung (mit std :: bind )

Ermöglicht das Einstellen einiger Parameter im Voraus, um beispielsweise eine Mitgliedsfunktion aufzurufen.

struct MyClass
{
    int DoStuff(double d); // actually a DoStuff(MyClass* this, double d)
};

std::function<int(double d)> f = std::bind(&MyClass::DoStuff, this, std::placeholders::_1);
// auto f = std::bind(...); in C++11

Option 7: Vorlagen

Akzeptieren Sie alles, solange es mit der Argumentliste übereinstimmt.

template <class FunctionT>
int DoSomething(FunctionT func)
{
    return func(3.14);
}
JN
quelle
2
Tolle Liste, +1. Allerdings zählen hier nur zwei wirklich als Delegierte - das Erfassen von Lambdas und des zurückgegebenen Objekts std::bind, und beide tun wirklich dasselbe, außer dass Lambdas nicht in dem Sinne polymorph sind, dass sie unterschiedliche Argumenttypen akzeptieren können.
Xeo
1
@JN: Warum empfehlen Sie, keine Funktionszeiger zu verwenden, scheinen aber mit der Verwendung der Elementmethodenzeiger einverstanden zu sein? Sie sind einfach identisch!
Matthieu M.
1
@MatthieuM. : gutes Argument. Ich habe Funktionszeiger als Vermächtnis betrachtet, aber das ist wahrscheinlich nur mein persönlicher Geschmack.
JN
1
@Xeo: Meine Vorstellung von einem Delegierten ist eher empirisch. Vielleicht mische ich zu viel Funktionsobjekt und delegiere (beschuldige meine frühere C # -Erfahrung).
JN
2
@ SirYakalot Etwas, das sich wie eine Funktion verhält, aber gleichzeitig den Status beibehält und wie jede andere Variable manipuliert werden kann. Eine Verwendung besteht beispielsweise darin, eine Funktion zu verwenden, die zwei Parameter verwendet, und den ersten Parameter zu zwingen, einen bestimmten Wert zu haben, um eine neue Funktion mit einem einzelnen Parameter ( bindingin meiner Liste) zu erstellen . Dies können Sie mit Funktionszeigern nicht erreichen.
JN
37

Ein Delegat ist eine Klasse, die einen Zeiger oder Verweis auf eine Objektinstanz umschließt, eine Mitgliedsmethode der Klasse dieses Objekts, die für diese Objektinstanz aufgerufen werden soll, und eine Methode zum Auslösen dieses Aufrufs bereitstellt.

Hier ist ein Beispiel:

template <class T>
class CCallback
{
public:
    typedef void (T::*fn)( int anArg );

    CCallback(T& trg, fn op)
        : m_rTarget(trg)
        , m_Operation(op)
    {
    }

    void Execute( int in )
    {
        (m_rTarget.*m_Operation)( in );
    }

private:

    CCallback();
    CCallback( const CCallback& );

    T& m_rTarget;
    fn m_Operation;

};

class A
{
public:
    virtual void Fn( int i )
    {
    }
};


int main( int /*argc*/, char * /*argv*/ )
{
    A a;
    CCallback<A> cbk( a, &A::Fn );
    cbk.Execute( 3 );
}
Grimm der Opiner
quelle
Welche Methode bietet das Auslösen des Anrufs? der Delegierte? Wie? Funktionszeiger?
SirYakalot
Die Delegate-Klasse bietet eine Methode an, mit Execute()der der Funktionsaufruf für das Objekt ausgelöst wird, das der Delegate umschließt.
Grimm The Opiner
4
Anstelle von Execute würde ich in Ihrem Fall dringend empfehlen, den Aufrufoperator zu überschreiben - void CCallback :: operator () (int). Der Grund dafür liegt in der generischen Programmierung. Es wird erwartet, dass aufrufbare Objekte wie eine Funktion aufgerufen werden. o.Execute (5) wäre nicht kompatibel, aber o (5) würde gut als aufrufbares Vorlagenargument passen. Diese Klasse könnte auch verallgemeinert werden, aber ich nehme an, Sie haben es der Kürze halber einfach gehalten (was eine gute Sache ist). Abgesehen von diesem Nitpick ist dies eine sehr nützliche Klasse.
David Peterson
17

Die Notwendigkeit von C ++ - Delegatenimplementierungen ist für die C ++ - Community eine dauerhafte Verlegenheit. Jeder C ++ - Programmierer würde sie gerne haben, also verwenden sie sie schließlich trotz der Tatsachen, dass:

  1. std::function() verwendet Heap-Operationen (und ist für ernsthafte eingebettete Programmierung unerreichbar).

  2. Alle anderen Implementierungen machen mehr oder weniger Zugeständnisse hinsichtlich der Portabilität oder Standardkonformität (überprüfen Sie dies bitte, indem Sie die verschiedenen Delegate-Implementierungen hier und im Codeprojekt überprüfen). Ich habe noch keine Implementierung gesehen, die keine wilden reinterpret_casts verwendet, verschachtelte Klassen- "Prototypen", die hoffentlich Funktionszeiger mit der gleichen Größe wie die vom Benutzer übergebenen erzeugen, Compiler-Tricks wie zuerst vorwärts deklarieren, dann typedef und dann erneut deklarieren. diesmal von einer anderen Klasse oder ähnlichen zwielichtigen Techniken erben. Während es eine großartige Leistung für die Implementierer ist, die das erstellt haben, ist es immer noch ein trauriges Zeugnis darüber, wie sich C ++ entwickelt.

  3. Nur selten wird darauf hingewiesen, dass die Delegierten nun über 3 C ++ - Standardrevisionen nicht richtig angesprochen wurden. (Oder das Fehlen von Sprachfunktionen, die eine einfache Implementierung von Delegierten ermöglichen.)

  4. Mit der Art und Weise, wie C ++ 11-Lambda-Funktionen durch den Standard definiert werden (jedes Lambda hat einen anonymen, unterschiedlichen Typ), hat sich die Situation nur in einigen Anwendungsfällen verbessert. Für den Anwendungsfall der Verwendung von Delegaten in (DLL-) Bibliotheks-APIs können Lambdas allein jedoch immer noch nicht verwendet werden. Die übliche Technik besteht darin, das Lambda zuerst in eine std :: -Funktion zu packen und es dann über die API zu übergeben.

BitTickler
quelle
Ich habe eine Version von Elbert Mais generischen Rückrufen in C ++ 11 erstellt, die auf anderen Ideen basiert, die an anderer Stelle zu sehen sind, und es scheint die meisten der Probleme zu klären, die Sie in 2) zitieren. Das einzige verbleibende Problem für mich ist, dass ich nicht mehr in der Lage bin, ein Makro im Client-Code zu verwenden, um die Delegaten zu erstellen. Es gibt wahrscheinlich einen magischen Ausweg für Vorlagen, den ich noch nicht gelöst habe.
kert
3
Ich verwende std :: function ohne Heap im eingebetteten System und es funktioniert immer noch.
Prasad
1
std::functionnicht immer dynamisch zuordnen
Lightness Races in Orbit
3
Diese Antwort ist eher eine Schimpfe als eine tatsächliche Antwort
Lightness Races in Orbit
7

Ein Delegat bietet ganz einfach Funktionen für die Funktionsweise eines Funktionszeigers. Es gibt viele Einschränkungen von Funktionszeigern in C ++. Ein Delegat verwendet eine Vorlage-Unangenehmkeit hinter den Kulissen, um ein Funktionszeiger-Element der Vorlagenklasse zu erstellen, das so funktioniert, wie Sie es möchten.

Das heißt, Sie können sie so einstellen, dass sie auf eine bestimmte Funktion zeigen, und Sie können sie weitergeben und aufrufen, wann und wo immer Sie möchten.

Hier gibt es einige sehr gute Beispiele:

Sir Yakalot
quelle
4

Eine Option für Delegierte in C ++, die hier nicht anders erwähnt wird, besteht darin, den C-Stil mit einer Funktion ptr und einem Kontextargument auszuführen. Dies ist wahrscheinlich das gleiche Muster, das viele, die diese Frage stellen, zu vermeiden versuchen. Das Muster ist jedoch portabel, effizient und kann in eingebettetem Code und Kernel-Code verwendet werden.

class SomeClass
{
    in someMember;
    int SomeFunc( int);

    static void EventFunc( void* this__, int a, int b, int c)
    {
        SomeClass* this_ = static_cast< SomeClass*>( this__);

        this_->SomeFunc( a );
        this_->someMember = b + c;
    }
};

void ScheduleEvent( void (*delegateFunc)( void*, int, int, int), void* delegateContext);

    ...
    SomeClass* someObject = new SomeObject();
    ...
    ScheduleEvent( SomeClass::EventFunc, someObject);
    ...
Joe
quelle
1

Windows-Laufzeitäquivalent eines Funktionsobjekts in Standard-C ++. Man kann die gesamte Funktion als Parameter verwenden (eigentlich ist das ein Funktionszeiger). Es wird hauptsächlich in Verbindung mit Ereignissen verwendet. Der Delegierte repräsentiert einen Vertrag, den Event-Handler viel erfüllen. Es erleichtert, wie ein Funktionszeiger funktionieren kann.

nmserve
quelle