Dovrei usare std :: function o un puntatore a funzione in C ++?


142

Quando implemento una funzione di callback in C ++, dovrei ancora usare il puntatore alla funzione in stile C:

void (*callbackFunc)(int);

O dovrei usare std :: function:

std::function< void(int) > callbackFunc;

9
Se la funzione di callback è nota al momento della compilazione, prendere in considerazione un modello.
Baum mit Augen

4
Quando si implementa una funzione di richiamata, è necessario fare tutto ciò che richiede il chiamante. Se la tua domanda riguarda davvero la progettazione di un'interfaccia di callback, non c'è nessun posto abbastanza vicino qui per rispondere. Cosa vuoi che faccia il destinatario del tuo callback? Di quali informazioni hai bisogno per passare al destinatario? Quali informazioni deve ricevere il destinatario a seguito della chiamata?
Pete Becker,

Risposte:


171

In breve, usa astd::function meno che tu non abbia un motivo per non farlo.

I puntatori a funzione hanno lo svantaggio di non essere in grado di acquisire alcuni contesti. Ad esempio, non sarà possibile passare una funzione lambda come callback che acquisisce alcune variabili di contesto (ma funzionerà se non ne acquisisce nessuna). Anche la chiamata di una variabile membro di un oggetto (cioè non statica) non è quindi possibile, poiché l'oggetto ( this-punto) deve essere catturato. (1)

std::function(dal momento che C ++ 11) è principalmente quello di memorizzare una funzione (passarla in giro non richiede che sia memorizzata). Pertanto, se si desidera archiviare il callback, ad esempio in una variabile membro, è probabilmente la scelta migliore. Ma anche se non lo memorizzi, è una buona "prima scelta" anche se ha lo svantaggio di introdurre un overhead (molto piccolo) quando viene chiamato (quindi in una situazione molto critica delle prestazioni potrebbe essere un problema ma nella maggior parte non dovrebbe). È molto "universale": se ti preoccupi molto del codice coerente e leggibile e non vuoi pensare a ogni scelta che fai (cioè vuoi mantenerlo semplice), usa std::functionper ogni funzione che passi.

Pensa a una terza opzione: se stai per implementare una piccola funzione che poi segnala qualcosa tramite la funzione di callback fornita, considera un parametro modello , che può quindi essere qualsiasi oggetto richiamabile , ad esempio un puntatore a funzione, un funzione, un lambda, a std::function, ... Lo svantaggio è che la tua funzione (esterna) diventa un modello e quindi deve essere implementata nell'intestazione. D'altra parte, si ottiene il vantaggio che la chiamata al callback può essere incorporata, poiché il codice client della funzione (esterna) "vede" la chiamata al callback con le informazioni esatte sul tipo disponibili.

Esempio per la versione con il parametro template (scrivere &invece che &&per pre-C ++ 11):

template <typename CallbackFunction>
void myFunction(..., CallbackFunction && callback) {
    ...
    callback(...);
    ...
}

Come puoi vedere nella tabella seguente, tutti hanno i loro vantaggi e svantaggi:

+-------------------+--------------+---------------+----------------+
|                   | function ptr | std::function | template param |
+===================+==============+===============+================+
| can capture       |    no(1)     |      yes      |       yes      |
| context variables |              |               |                |
+-------------------+--------------+---------------+----------------+
| no call overhead  |     yes      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be inlined    |      no      |       no      |       yes      |
| (see comments)    |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be stored     |     yes      |      yes      |      no(2)     |
| in class member   |              |               |                |
+-------------------+--------------+---------------+----------------+
| can be implemented|     yes      |      yes      |       no       |
| outside of header |              |               |                |
+-------------------+--------------+---------------+----------------+
| supported without |     yes      |     no(3)     |       yes      |
| C++11 standard    |              |               |                |
+-------------------+--------------+---------------+----------------+
| nicely readable   |      no      |      yes      |      (yes)     |
| (my opinion)      | (ugly type)  |               |                |
+-------------------+--------------+---------------+----------------+

(1) Esistono soluzioni alternative per superare questa limitazione, ad esempio passando i dati aggiuntivi come ulteriori parametri alla funzione (esterna): myFunction(..., callback, data)chiamerà callback(data). Questo è il "callback con argomenti" in stile C, che è possibile in C ++ (e comunque ampiamente utilizzato nell'API WIN32) ma dovrebbe essere evitato perché abbiamo opzioni migliori in C ++.

(2) A meno che non stiamo parlando di un modello di classe, ovvero la classe in cui memorizzi la funzione è un modello. Ciò significherebbe che sul lato client il tipo di funzione decide il tipo di oggetto che memorizza il callback, che non è quasi mai un'opzione per i casi d'uso reali.

(3) Per pre-C ++ 11, utilizzare boost::function


9
i puntatori a funzione hanno un overhead di chiamata rispetto ai parametri del modello. i parametri del modello semplificano l'inline, anche se si passano da un livello all'altro di quindici livelli, poiché il codice in esecuzione è descritto dal tipo di parametro e non dal valore. E gli oggetti funzione modello archiviati nei tipi restituiti modello sono un modello comune e utile (con un buon costruttore di copie, è possibile creare l'invocabile funzione modello efficiente che può essere convertita in quella std::functioncancellata dal tipo se è necessario memorizzarla all'esterno chiamato contesto).
Yakk - Adam Nevraumont,

1
@tohecz Ora menziono se richiede C ++ 11 o meno.
salva il

1
@Yakk Oh, certo, me ne sono dimenticato! Aggiunto, grazie.
salva il

1
@MooingDuck Ovviamente dipende dall'implementazione. Ma se ricordo bene, a causa di come funziona la cancellazione del tipo, si sta verificando un'altra indiretta? Ma ora che ci ripenso, immagino che non sia così se si assegnano puntatori a funzioni o lambda senza acquisizione ... (come una tipica ottimizzazione)
leemes

1
@leemes: Giusto, per i puntatori a funzione o lambda senza capotasto, dovrebbe avere lo stesso overhead di un c-func-ptr. Che è ancora una stalla della pipeline + non banalmente in linea.
Mooing Duck,

25

void (*callbackFunc)(int); può essere una funzione di callback in stile C, ma è orribilmente inutilizzabile con un design scadente.

Sembra un callback in stile C ben progettato void (*callbackFunc)(void*, int);: deve void*consentire al codice che esegue il callback di mantenere lo stato oltre la funzione. Non farlo costringe il chiamante a memorizzare lo stato a livello globale, il che è scortese.

std::function< int(int) >finisce per essere leggermente più costoso int(*)(void*, int)dell'invocazione nella maggior parte delle implementazioni. Tuttavia, per alcuni compilatori è più difficile inline. Esistono std::functionimplementazioni di cloni che rivaleggiano con le spese generali di invocazione dei puntatori di funzione (vedere "delegati più veloci possibili", ecc.) Che possono entrare nelle librerie.

Ora, i client di un sistema di callback devono spesso impostare risorse e smaltirle quando il callback viene creato e rimosso e essere consapevoli della durata del callback. void(*callback)(void*, int)non fornisce questo.

A volte questo è disponibile tramite la struttura del codice (il callback ha una durata limitata) o tramite altri meccanismi (annulla la registrazione di callback e simili).

std::function fornisce un mezzo per la gestione a vita limitata (l'ultima copia dell'oggetto scompare quando viene dimenticata).

In generale, userei un std::functionmanifest a meno che non riguardi le prestazioni. Se lo facessero, cercherei prima i cambiamenti strutturali (invece di un callback per pixel, che ne dici di generare un processore scanline basato sul lambda che mi passi? Che dovrebbe essere sufficiente per ridurre l'overhead della chiamata di funzione a livelli banali. ). Quindi, se persiste, scrivo un delegatedelegato basato sul più veloce possibile e vedo se il problema delle prestazioni scompare.

Utilizzerei principalmente i puntatori a funzione per API legacy o per la creazione di interfacce C per la comunicazione tra codice generato da diversi compilatori. Li ho anche usati come dettagli di implementazione interni quando sto implementando tabelle di salto, tipo di cancellazione, ecc: quando lo sto producendo e consumando, e non lo sto esponendo esternamente per l'uso di qualsiasi codice client, e i puntatori a funzione fanno tutto ciò di cui ho bisogno .

Si noti che è possibile scrivere wrapper che trasformano a std::function<int(int)>in uno int(void*,int)stile di callback, supponendo che vi sia una corretta infrastruttura di gestione della durata del callback. Quindi, come test del fumo per qualsiasi sistema di gestione della durata della richiamata in stile C, mi assicurerei che il confezionamento funzioni std::functionragionevolmente bene.


1
Da dove void*viene? Perché vorresti mantenere lo stato oltre la funzione? Una funzione dovrebbe contenere tutto il codice necessario, tutte le funzionalità, basta passare gli argomenti desiderati e modificare e restituire qualcosa. Se hai bisogno di uno stato esterno, perché una funzionePtr o callback dovrebbe trasportare quel bagaglio? Penso che il callback sia inutilmente complesso.
Nikos,

@ nik-lz Non sono sicuro di come ti insegnerei l'uso e la cronologia dei callback in C in un commento. O la filosofia della procedura rispetto alla programmazione funzionale. Quindi, lascerai incompiuto.
Yakk - Adam Nevraumont,

Ho dimenticato this. È perché si deve tenere conto del caso in cui viene chiamata una funzione membro, quindi abbiamo bisogno del thispuntatore per puntare all'indirizzo dell'oggetto? Se sbaglio potresti darmi un link dove posso trovare maggiori informazioni su questo, perché non riesco a trovare molto al riguardo. Grazie in anticipo.
Nikos,

@ Le funzioni membro di Nik-Lz non sono funzioni. Le funzioni non hanno uno stato (di runtime). I callback richiedono a void*per consentire la trasmissione dello stato di runtime. Un puntatore a funzione con a void*e un void*argomento può emulare una chiamata di funzione membro a un oggetto. Siamo spiacenti, non conosco una risorsa che passi attraverso "la progettazione di meccanismi di callback C 101".
Yakk - Adam Nevraumont,

Sì, è di questo che stavo parlando. Lo stato di runtime è sostanzialmente l'indirizzo dell'oggetto chiamato (perché cambia tra le esecuzioni). Si tratta ancora this. Ecco cosa intendevo. Ok grazie lo stesso.
Nikos,

17

Utilizzare std::functionper memorizzare oggetti richiamabili arbitrari. Consente all'utente di fornire qualunque contesto sia necessario per la richiamata; un semplice puntatore a funzione no.

Se hai bisogno di usare semplici puntatori a funzioni per qualche motivo (forse perché vuoi un'API compatibile con C), allora dovresti aggiungere un void * user_contextargomento in modo che sia almeno possibile (anche se scomodo) che acceda allo stato che non è passato direttamente al funzione.


Qual è il tipo di p qui? sarà un tipo di funzione std ::? void f () {}; auto p = f; p ();
sree,

14

L'unico motivo per evitare std::functionè il supporto di compilatori legacy che non supportano questo modello, che è stato introdotto in C ++ 11.

Se il supporto del linguaggio pre-C ++ 11 non è un requisito, l'utilizzo std::functionoffre ai chiamanti una scelta più ampia nell'implementazione del callback, rendendolo un'opzione migliore rispetto ai puntatori di funzione "semplici". Offre agli utenti della tua API una scelta più ampia, mentre astrarre le specifiche della loro implementazione per il tuo codice che esegue il callback.


1

std::function può portare VMT nel codice in alcuni casi, il che ha un impatto sulle prestazioni.


3
Puoi spiegare che cos'è questo VMT?
Gupta,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.