2012-11-29 14 views
9

In OO, uno di solito implementa callback con interfacce: (esempio di massima)Richiamata con interfacce o oggetti funzione?

class Message {} 

class IMsgProcessor { 
public: 
    virtual void handle_msg(const Message& msg) = 0; 
} 

class RequestMsgProcessor : public IMsgProcessor { 
    virtual void handle_msg(const Message& msg) { 
    // process request message 
    } 
} 

class CustomSocket { 
public: 
    Socket(IMsgProcessor* p) : processor_(p) {} 

    void receive_message_from_network(const Message& msg) { 
     // processor_ does implement handle_msg. Otherwise a compile time error. 
     // So we've got a safe design. 
     processor_->handle_msg(msg); 
    } 
private: 
    IMsgProcessor* processor_; 
} 

Fin qui tutto bene. Con C++ 11, un altro modo per farlo è quello di avere CustomSocket appena ricevuto un'istanza di oggetto std :: function. Non importa dove si è implementato o anche se l'oggetto è un valore non nullo:

class CustomSocket { 
public: 
    Socket(std::function<void(const Message&)>&& f) : func_(std:forward(f)) {} 

    void receive_message_from_network(const Message& msg) { 
     // unfortunately we have to do this check for every msg. 
     // or maybe not ... 
     if(func_) 
      func_(msg); 
    } 
private: 
    std::function<void(const Message&)> func_; 
} 

Ora qui sono le domande:
1. Che cosa circa gli impatti delle prestazioni? Immagino che una chiamata a una funzione virtuale sia più veloce della chiamata a un oggetto funzione ma quanto più veloce? Sto implementando un sistema di messaggistica veloce e preferirei evitare qualsiasi penalizzazione delle prestazioni non necessaria.
2. In termini di pratiche di ingegneria del software, devo dire che mi piace il secondo approccio migliore. Meno codice, meno file, meno ingombro: nessuna classe di interfaccia. Maggiore flessibilità: è possibile implementare solo un sottoinsieme dell'interfaccia impostando alcuni degli oggetti funzione e lasciando gli altri nulli. Oppure puoi avere diverse parti dell'interfaccia implementate in classi separate o funzioni libere o una combinazione di entrambi (invece che in una singola sottoclasse). Inoltre, CustomSocket può essere utilizzato da qualsiasi classe, non solo sottoclassi di IMsgProcessor. Questo è un vantaggio , a mio parere.
Cosa rispondi? Vedi qualche difetto fondamentale in questi argomenti?

+0

'// Purtroppo dobbiamo fare questo controllo per ogni msg.' - la stessa cosa deve essere vero per il codice di interfaccia. Non si controlla mai se non si ottiene un puntatore nullo ... – Xeo

+0

Informazioni su perfs: la funzione virtuale in pratica aggiunge una ricerca tabella, std :: function inoltra le chiamate a un adattatore specializzato. Se la tua funzione/metodo non è virtuale, potrebbe essere ottimizzato per essere il più veloce. Devo fare un punto di riferimento. Se si sostituisce 'std :: function' con un argomento modello, è possibile renderlo più veloce della chiamata virtuale. – Antoine

+0

@Xeo Non necessariamente: nel codice dell'interfaccia vorrei solo verificare in costruttore che l'oggetto passato non fosse NULL - accettare un NULL non ha senso. Ma nel secondo caso perché è possibile avere un'implementazione parziale dell'interfaccia (ad esempio, solo alcuni oggetti funzione forse non null) è necessario il controllo. – PoP

risposta

3

si può avere il meglio dei due mondi

template<class F> 
class MsgProcessorT:public IMsgProcessor{ 
    F f_; 
    public: 
    MsgProcessorT(F f):f_(f){} 
    virtual void handle_msg(const Message& msg) { 
     f_(msg); 
} 

}; 
template<class F> 
IMsgProcessor* CreateMessageProcessor(F f){ 
    return new MsgProcessor<T>(f); 

}; 

allora si può usare come questo

Socket s(CreateMessageProcessor([](const Message& msg){...})); 

O, per rendere ancora più facile aggiungere un altro costruttore alla presa

class Socket{ 
... 
template<class F> 
Socket(F f):processor_(CreateMessageProcessor(f){} 


}; 

Allora potreste fare

Socket s([](const Message& msg){...}); 

e avere ancora la stessa efficienza come una funzione chiamata virtuale

+4

Ew, nuda 'nuova'! Copri la tua vergogna! ... Su una nota più seria, 'std :: function' utilizza internamente una distribuzione virtuale integrata o un'implementazione manuale con i puntatori di funzione,' void * 'e i modelli di funzione. – Xeo

+0

@Xeo Hai ragione. Vorrei rendere Socket non-copyable e usare unique_ptr per processor_ e restituire un unique_ptr da CreateMessageProcessor. –

+0

@Xeo: Haha, questo mi ha fatto crollare. : -] – ildjarn

2

L'approccio dell'interfaccia è più tradizionale ma anche più dettagliato: è chiaro cosa fa un MessageProcessor e non è necessario controllarlo. Inoltre, puoi riutilizzare lo stesso oggetto con molti socket.

L'approccio std::function è più generale: è possibile utilizzare qualsiasi cosa che accetti lo operator()(Message const&). Tuttavia, la mancanza di verbosità ha il rischio di rendere il codice meno leggibile.

Non so di sanzioni sulle prestazioni, ma sarei sorpreso se ci sono differenze significative.

Vorrei attenermi all'approccio dell'interfaccia se questa è una parte essenziale del codice (come sembra essere).

1

In entrambi gli esempi si stanno effettivamente utilizzando le interfacce. Ciò che è diverso è il modo in cui li definisci. Nel primo caso l'interfaccia è una classe tradizionale con pure funzioni virtuali e nel secondo caso l'interfaccia è un riferimento di funzione - è molto più diversa da un puntatore di funzione C dal punto di vista del progetto. Secondo me è possibile combinare entrambe le varianti in base ai requisiti specifici e considerare i pro ei contro (che sono come hai dichiarato) per ogni nuovo caso. Per quanto riguarda l'impatto sulle prestazioni, ritengo che la migliore risposta sarebbe quella di eseguire test, confrontare i risultati e abbinare i requisiti di prestazione.

Problemi correlati