2013-03-18 3 views
6

Ich möchte in einer Sammlung mit ähnlichen Signaturfunktionen speichern, zu tun:Gibt es eine idiomatische Möglichkeit, eine Sammlung von Delegaten in C++ zu erstellen? so etwas wie diese

f(vector<Order>& orders, vector<Function>& functions) { 
    foreach(process_orders in functions) process_orders(orders); 
} 

ich von Funktionszeigern dachte:

void GiveCoolOrdersToBob(Order); 
void GiveStupidOrdersToJohn(Order); 

typedef void (*Function)(Order); 
vector<Function> functions; 
functions.push_back(&GiveStupidOrdersToJohn); 
functions.push_back(&GiveCoolOrdersToBob); 

oder polymorphe Funktionsobjekte:

struct IOrderFunction { 
    virtual void operator()(Order) = 0; 
} 

struct GiveCoolOrdersToBob : IOrderFunction { 
    ... 
} 

struct GiveStupidOrdersToJohn : IOrderFunction { 
    ... 
} 

vector<IOrderFunction*> functions; 
functions.push_back(new GiveStupidOrdersToJohn()); 
functions.push_back(new GiveCoolOrdersToBob()); 
+0

http://www.boost.org/doc/libs/1_53_0/doc/html/signals2.html könnte einen Blick für dieses Problem Raum –

Antwort

1

Vielleicht möchten Sie in std::function suchen, würde Ihr Vektor dann so aussehen:

std::vector< std::function< void(Order) > > functions; 

Aber beachten Sie, dass std::function einen geringen Overhead hat. Für die Fälle, fallen die new:

function.push_back(GiveStupidOrdersToJohn()); 
0

Boost.Signal genau Ihr Problem löst. Das sollten Sie sich ansehen. Es sei denn, Sie haben spezielle Anforderungen. Insbesondere verwenden boost.signal und boost.function und/oder std :: function Typenlöschtechniken. Somit können Sie einen Vektor aufrufbarer Dinge mit einer bestimmten Signatur haben. Es spielt keine Rolle, ob Ihre Entitäten einfache C-Funktionen (wie in Ihrem Beispiel) oder Funktionsobjekte oder Member-Funktionen im Allgemeinen sind. Sie können alle mischen.

9

Premise:

Der Entwurf Sie arbeiten wird vorschlagen, aber regelmäßige Funktionszeiger verwendet werden Sie erheblich in der Art der Rückrufe beschränken Sie sich registrieren können, und zwar stärker, der Ansatz basiert auf Vererbung von einer festen Oberfläche ist Ausführlicher und erfordert mehr Arbeit für einen Client, um Rückrufe zu definieren.

In dieser Antwort werde ich zuerst einige Beispiele zeigen, wie man std::function für diesen Zweck verwendet. Die Beispiele sprechen ziemlich für sich selbst und zeigen, wie und warum die Verwendung von std::function Vorteile im Gegensatz zu den von Ihnen beschriebenen Lösungen bringt.

Allerdings wird ein naive Ansatz basierend auf std::function auch eigene Einschränkungen haben, die ich auflisten werde. Deshalb schlage ich Ihnen schließlich vor, einen Blick auf Boost.Signals2 zu werfen: Es ist eine ziemlich leistungsfähige und einfach zu bedienende Bibliothek. Ich werde Boost.Signals2 am Ende dieser Antwort ansprechen. Hoffentlich wird es Ihnen leichter fallen, die komplexeren Aspekte von Signalen und Slots später zu verstehen, wenn Sie zuerst ein einfaches Design basierend auf std::function verstehen.


Lösung auf Basis von std :: function <>

Lasst uns ein paar einfache Klassen einführen und den Boden für einige konkrete Beispiele vor. Hier ist ein order etwas, das eine id hat und mehrere item s enthält. Jede item wird von einem type beschrieben (der Einfachheit halber hier kann es entweder ein Buch eine DVD sein) und eine name:

#include <vector> 
#include <memory> 
#include <string> 

struct item // A very simple data structure for modeling order items 
{ 
    enum type { book, dvd }; 

    item(type t, std::string const& s) : itemType(t), name(s) { } 

    type itemType; // The type of the item 

    std::string name; // The name of the item 
}; 

struct order // An order has an ID and contains a certain number of items 
{ 
    order(int id) : id(id) { } 

    int get_id() const { return id; } 

    std::vector<item> const& get_items() const { return items; } 

    void add_item(item::type t, std::string const& n) 
    { items.emplace_back(t, n); } 

private: 

    int id; 
    std::vector<item> items; 
}; 

Das Herz der Lösung, die ich skizzieren werde, ist die folgende Klasse order_repository und seine interne Verwendung von std::function, um von Kunden registrierte Rückrufe zu halten.

Rückrufe durch die register_callback() Funktion registriert werden können, und (ganz intuitiv) unregistrierte durch die unregister_callback() Funktion durch das Cookie von registered_callback() bei der Anmeldung zurück Bereitstellung:

Die Funktion als eine Aufträge für die Platzierung place_order() Funktion hat, und ein process_order() Funktion, die die Verarbeitung aller Aufträge auslöst. Dies führt dazu, dass alle registrierten Handler nacheinander aufgerufen werden. Jeder Handler erhält einen Verweis auf den gleichen Vektor von Bestellungen:

#include <functional> 

using order_ptr = std::shared_ptr<order>; // Just a useful type alias 

class order_repository // Collects orders and registers processing callbacks 
{ 

public: 

    typedef std::function<void(std::vector<order_ptr>&)> order_callback; 

    template<typename F> 
    size_t register_callback(F&& f) 
    { return callbacks.push_back(std::forward<F>(f)); } 

    void place_order(order_ptr o) 
    { orders.push_back(o); } 

    void process_all_orders() 
    { for (auto const& cb : callbacks) { cb(orders); } } 

private: 

    std::vector<order_callback> callbacks; 
    std::vector<order_ptr> orders; 
}; 

Die Stärke dieser Lösung aus der Verwendung von std::function kommt Typ Löschung und allow encapsulating any kind of callable object zu realisieren.

Die folgende Hilfsfunktion, die wir einige Aufträge zu generieren und zu platzieren, vervollständigt die Einrichtung (es einfach erstellt vier Aufträge und fügt ein paar Elemente zu jeder Bestellung) verwenden:

void generate_and_place_orders(order_repository& r) 
{ 
    order_ptr o = std::make_shared<order>(42); 
    o->add_item(item::book, "TC++PL, 4th Edition"); 
    r.place_order(o); 

    o = std::make_shared<order>(1729); 
    o->add_item(item::book, "TC++PL, 4th Edition"); 
    o->add_item(item::book, "C++ Concurrency in Action"); 
    r.place_order(o); 

    o = std::make_shared<order>(24); 
    o->add_item(item::dvd, "2001: A Space Odyssey"); 
    r.place_order(o); 

    o = std::make_shared<order>(9271); 
    o->add_item(item::dvd, "The Big Lebowski"); 
    o->add_item(item::book, "C++ Concurrency in Action"); 
    o->add_item(item::book, "TC++PL, 4th Edition"); 
    r.place_order(o); 
} 

Sehen wir uns nun an, welche Arten von Callback wir anbieten können. Für Starter, lassen Sie uns eine regelmäßige Callback-Funktion, die alle Aufträge druckt:

void print_all_orders(std::vector<order_ptr>& orders) 
{ 
    std::cout << "Printing all the orders:\n=========================\n"; 
    for (auto const& o : orders) 
    { 
     std::cout << "\torder #" << o->get_id() << ": " << std::endl; 

     int cnt = 0; 
     for (auto const& i : o->get_items()) 
     { 
      std::cout << "\t\titem #" << ++cnt << ": (" 
         << ((i.itemType == item::book) ? "book" : "dvd") 
         << ", " << "\"" << i.name << "\")\n"; 
     } 
    } 

    std::cout << "=========================\n\n"; 
} 

Und ein einfaches Programm, das es verwendet:

int main() 
{ 
    order_repository r; 
    generate_and_place_orders(r); 

    // Register a regular function as a callback... 
    r.register_callback(print_all_orders); 

    // Process the order! (Will invoke all the registered callbacks) 
    r.process_all_orders(); 
} 

Hier ist die live example die Ausgabe dieses Programms zeigt.

Ganz recht, Sie sind nur zu Registrierung reguläre Funktionen nicht darauf beschränkt: jedes aufrufbare Objekt kann, darunter einen Funktors als Rückruf registriert wird einige Statusinformationen zu halten. Lassen Sie sich die obige Funktion als Funktors umschreiben, die entweder die gleiche detaillierte Liste der Aufträge als Funktion print_all_orders() oben drucken können, oder eine kürzere Zusammenfassung, die nicht Auftragspositionen nicht enthalten:

struct print_all_orders 
{ 
    print_all_orders(bool detailed) : printDetails(detailed) { } 

    void operator() (std::vector<order_ptr>& orders) 
    { 
     std::cout << "Printing all the orders:\n=========================\n"; 
     for (auto const& o : orders) 
     { 
      std::cout << "\torder #" << o->get_id(); 
      if (printDetails) 
      { 
       std::cout << ": " << std::endl; 
       int cnt = 0; 
       for (auto const& i : o->get_items()) 
       { 
        std::cout << "\t\titem #" << ++cnt << ": (" 
           << ((i.itemType == item::book) ? "book" : "dvd") 
           << ", " << "\"" << i.name << "\")\n"; 
       } 
      } 
      else { std::cout << std::endl; } 
     } 

     std::cout << "=========================\n\n"; 
    } 

private: 

    bool printDetails; 
}; 

Hier ist, wie dies in a verwendet werden könnte kleines Testprogramm:

int main() 
{ 
    using namespace std::placeholders; 

    order_repository r; 
    generate_and_place_orders(r); 

    // Register one particular instance of our functor... 
    r.register_callback(print_all_orders(false)); 

    // Register another instance of the same functor... 
    r.register_callback(print_all_orders(true)); 

    r.process_all_orders(); 
} 

Und hier ist der entsprechende Ausgang in this live example gezeigt.

Dank der Flexibilität von std::function können wir auch das Ergebnis von std::bind() als Rückruf registrieren. Um dies zu demonstrieren mit einem Beispiel, lassen einzuführen ist eine weitere Klasse person:

#include <iostream> 

struct person 
{ 
    person(std::string n) : name(n) { } 

    void receive_order(order_ptr spOrder) 
    { std::cout << name << " received order " << spOrder->get_id() << std::endl; } 

private: 

    std::string name; 
}; 

Klasse person hat eine Memberfunktion receive_order(). Das Aufrufen von receive_order() auf einem bestimmten person Objekt modelliert die Tatsache, dass ein bestimmtes order an das person geliefert wurde.

Wir konnten die Klassendefinition oben eine Callback-Funktion zu registrieren, die alle Aufträge an eine Person entsendet (, die zur Laufzeit bestimmt werden kann!):

void give_all_orders_to(std::vector<order_ptr>& orders, person& p) 
{ 
    std::cout << "Dispatching orders:\n=========================\n"; 
    for (auto const& o : orders) { p.receive_order(o); } 
    orders.clear(); 
    std::cout << "=========================\n\n"; 
} 

An dieser Stelle ich das folgende Programm schreiben könnte, registrieren, dass zwei Rückrufe: die gleiche Funktion zum Drucken von Aufträgen, die wir verwendet haben, und die obige Funktion für Aufträge zu einer bestimmten Instanz Dispatching von Person. Hier ist, wie wir es tun:

int main() 
{ 
    using namespace std::placeholders; 

    order_repository r; 
    generate_and_place_orders(r); 

    person alice("alice"); 

    r.register_callback(print_all_orders); 

    // Register the result of binding a function's argument... 
    r.register_callback(std::bind(give_all_orders_to, _1, std::ref(alice))); 

    r.process_all_orders(); 
} 

Die Ausgabe dieses Programms wird in this live example gezeigt.

Und natürlich könnte man lambdas als Rückrufe verwenden. Das folgende Programm baut auf den vorherigen die Verwendung eines Lambda-Rückruf zu zeigen, dass kleine Aufträge an eine Person entsendet, und große Aufträge an eine andere Person:

int main() 
{ 
    order_repository r; 
    generate_and_place_orders(r); 

    person alice("alice"); 
    person bob("bob"); 

    r.register_callback(print_all_orders); 
    r.register_callback([&] (std::vector<order_ptr>& orders) 
    { 
     for (auto const& o : orders) 
     { 
      if (o->get_items().size() < 2) { bob.receive_order(o); } 
      else { alice.receive_order(o); } 
     } 

     orders.clear(); 
    }); 

    r.process_all_orders(); 
} 

Wieder einmal this live example zeigt die entsprechende Ausgabe.


Jenseits std :: function <> (Boost.Signals2)

Die obige Konstruktion ist relativ einfach, sehr flexibel und einfach zu bedienen. Es gibt jedoch viele Dinge, die nicht erlaubt sind:

  • Es ist nicht erlaubt, das Dispatching von Ereignissen zu einem bestimmten Rückruf einfach einzufrieren und wieder aufzunehmen;
  • Es kapselt keine Sätze von zugehörigen Rückrufen zu einem Ereignis Klasse;
  • es nicht erlaubt, Rückrufe zu gruppieren und sie zu ordnen;
  • Callbacks können keine Werte zurückgeben;
  • es erlaubt nicht kombiniert diese Rückgabewerte.
  • Alle diese Funktionen, zusammen mit vielen anderen, werden von vollwertigen Bibliotheken wie Boost.Signals2 zur Verfügung gestellt, die Sie sich vielleicht ansehen möchten. Wenn Sie mit dem obigen Design vertraut sind, wird es Ihnen leichter fallen zu verstehen, wie es funktioniert.

    Zum Beispiel ist dies, wie Sie ein Signal definieren und zwei einfache Rückrufe registrieren, und rufen sie beide durch das Signal der Call-Betreiber Aufruf (aus der verknüpften Dokumentationsseite):

    struct Hello 
    { 
        void operator()() const 
        { 
         std::cout << "Hello"; 
        } 
    }; 
    
    struct World 
    { 
        void operator()() const 
        { 
         std::cout << ", World!" << std::endl; 
        } 
    }; 
    
    int main() 
    { 
        boost::signals2::signal<void()> sig; 
    
        sig.connect(Hello()); 
        sig.connect(World()); 
    
        sig(); 
    } 
    

    Wie üblich, ist hier a live example für das obige Programm.

    +1

    ........ wow. * Golfklatschen * – justderb

    Verwandte Themen