Domanda

Diciamo che ho una funzione che accetta un puntatore a funzione void (*)(void*) per l'uso come un callback:

void do_stuff(void (*callback_fp)(void*), void* callback_arg);

Ora, se ho una funzione come questa:

void my_callback_function(struct my_struct* arg);

Posso farlo in modo sicuro?

do_stuff((void (*)(void*)) &my_callback_function, NULL);

Ho guardato questa domanda e ho guardato alcuni standard C, che dicono che si può lanciare a 'puntatori a funzione compatibili', ma non riesco a trovare una definizione di ciò che significa 'puntatore a funzione compatibile'.

È stato utile?

Soluzione

Per quanto riguarda la serie C è interessato, se lanci un puntatore a funzione a un puntatore a funzione di tipo diverso e poi chiami, è comportamento non definito . Vedi allegato J.2 (informativa):

  

Il comportamento non è definito nelle seguenti circostanze:

     
      
  • Un puntatore viene utilizzato per chiamare una funzione il cui tipo non è compatibile con la punta-to   tipo (6.3.2.3).
  •   

Sezione 6.3.2.3, paragrafo 8 si legge:

  

Un puntatore a una funzione di un tipo può essere convertito in un puntatore a una funzione di un'altra   tipo e viceversa; il risultato deve confrontare uguale al puntatore originale. Se un convertito   puntatore viene utilizzato per chiamare una funzione il cui tipo non è compatibile con la punta-per digitare,   il comportamento è indefinito.

Quindi, in altre parole, è possibile lanciare un puntatore a funzione a un diverso tipo di puntatore a funzione, lanciarla di nuovo, e lo chiamano, e le cose funzioneranno.

La definizione di compatibile è un po 'complicato. Si può trovare nella sezione 6.7.5.3, paragrafo 15:

  

Per due tipi di funzione per essere compatibili, entrambe devono specificare i tipi di ritorno compatibili 127 .

     

Inoltre, le liste di tipo parametro, se entrambi sono presenti, sono d'accordo nel numero di   parametri e nell'uso del terminatore ellissi; parametri corrispondenti hanno   tipi compatibili. Se un tipo ha una lista tipo di parametro e l'altro tipo è specificato da un   funzione dichiaratore che non fa parte di una definizione di funzione e che contiene un vuoto   elenco degli identificatori, l'elenco dei parametri non deve avere un terminatore puntini di sospensione e il tipo di ogni   parametro deve essere compatibile con il tipo che risulta dall'applicazione della   promozioni di argomento predefinito. Se un tipo ha una lista tipo di parametro e l'altro tipo è   specificato da una definizione di funzione che contiene un (eventualmente vuoto) lista identificatore, entrambi devono   concordano nel numero di parametri, e il tipo di ciascun parametro prototipo sarà   compatibile con il tipo che risulta dall'applicazione dell'argomento predefinito   promozioni al tipo del corrispondente identificatore. (Nella determinazione del tipo   compatibilità e di tipo composito, ciascun parametro dichiarato con funzione o array   tipo è considerato come avente il tipo impostato e ogni parametro dichiarato con tipo qualificata   è preso come avere la versione senza riserve del suo tipo dichiarato.)

     

127) Se entrambi i tipi di funzione sono ‘‘vecchio stile’’, tipi di parametri non vengono confrontati.

Le regole per stabilire se due tipi sono compatibili sono descritti nella sezione 6.2.7, e non voglio citare qui dato che sono piuttosto lunga, ma li si può leggere sul bozza dello standard C99 (PDF) .

La regola rilevante è nella sezione 6.7.5.1, paragrafo 2:

  

Per due tipi di puntatore per essere compatibili, entrambe devono essere identico qualificato ed entrambi devono essere puntatori a tipi compatibili.

Quindi, poiché un void* non è compatibile con struct my_struct*, un puntatore a funzione di tipo void (*)(void*) non è compatibile con un puntatore funzione del tipo void (*)(struct my_struct*), quindi questo casting di puntatori a funzione è tecnicamente comportamento indefinito.

In pratica, però, si può tranquillamente ottenere via con puntatori a funzione di colata in alcuni casi. Nella convenzione x86 chiamata, argomenti vengono inseriti nello stack, e tutti i puntatori sono le stesse dimensioni (4 byte x 86 o 8 byte x86_64). Chiamare un puntatore a funzione riduce a spingere gli argomenti sullo stack e facendo un salto indiretto alla funzionebersaglio puntatore, e non c'è ovviamente alcuna nozione di tipi a livello di codice macchina.

Le cose che sicuramente non possono fare:

  • Fusioni tra puntatori a funzione di diverse convenzioni di chiamata. Potrai rovinare la pila e nella migliore delle ipotesi, l'arresto, nel peggiore dei casi, riescono in silenzio con un enorme buco di sicurezza a bocca aperta. Nella programmazione di Windows, spesso si passa puntatori a funzione in giro. Win32 si aspetta che tutte le funzioni di callback per utilizzare la convenzione stdcall chiamando (che la macro CALLBACK, PASCAL e WINAPI tutto si espandono a). Se si passa un puntatore a funzione che utilizza la convenzione di chiamata C standard (cdecl), cattiveria si tradurrà.
  • In C ++, gettato tra i puntatori a funzione membro di classe e puntatori a funzione regolari. Interviene spesso fino newbies C ++. funzioni membro della classe hanno un parametro this nascosta, e se lanci una funzione membro ad una funzione normale, non c'è oggetto this da usare, e di nuovo, molto più cattiveria si tradurrà.

Un'altra cattiva idea che potrebbe a volte funziona, ma è anche un comportamento indefinito:

  • Casting tra puntatori a funzione e puntatori regolari (per esempio lanciare un void (*)(void) ad un void*). puntatori a funzione non sono necessariamente le stesse dimensioni di puntatori regolari, dal momento che su alcune architetture che potrebbero contenere informazioni supplementari contestuale. Questo probabilmente funzionerà bene su x86, ma ricordate che si tratta di un comportamento indefinito.

Altri suggerimenti

ho chiesto informazioni su questo esattamente lo stesso problema per quanto riguarda un certo codice in GLib di recente. (GLib è una libreria di base per il progetto GNOME e scritto in C.) mi è stato detto tutto lo slots'n'signals quadro dipende da questo.

In tutto il codice, ci sono numerosi casi di fusione dal tipo (1) a (2):

  1. typedef int (*CompareFunc) (const void *a, const void *b)
  2. typedef int (*CompareDataFunc) (const void *b, const void *b, void *user_data)

E 'comune a catena attraverso chiamate come questa:

int stuff_equal (GStuff      *a,
                 GStuff      *b,
                 CompareFunc  compare_func)
{
    return stuff_equal_with_data(a, b, (CompareDataFunc) compare_func, NULL);
}

int stuff_equal_with_data (GStuff          *a,
                           GStuff          *b,
                           CompareDataFunc  compare_func,
                           void            *user_data)
{
    int result;
    /* do some work here */
    result = compare_func (data1, data2, user_data);
    return result;
}

Guardate voi stessi qui a g_array_sort(): http: //git.gnome .org / browse / glib / albero / glib / garray.c

Le risposte di cui sopra sono dettagliate e probabilmente corretto - se si siede sul comitato di standardizzazione. Adam e Johannes meritano credito per le loro risposte ben studiate. Tuttavia, fuori nel selvaggio, si trova questo codice funziona bene. Controverso? Sì. Considerate questo: GLib compila / lavori / test su un gran numero di piattaforme (Linux / Solaris / Windows / OS X) con una vasta gamma di compilatori / linker / caricatori kernel (GCC / Clang / MSVC). Standard di essere dannato, immagino.

Ho trascorso qualche tempo a pensare a queste risposte. Ecco la mia conclusione:

  1. Se si sta scrivendo una libreria di richiamata, questo potrebbe essere OK. Caveat emptor -. utilizzare a proprio rischio
  2. Else, non farlo.

Pensando più profondo dopo aver scritto questa risposta, io non sarei sorpreso se il codice per compilatori C usa questo stesso trucco. E dal momento che (la maggior parte / tutti?) Moderni compilatori C sono bootstrap, ciò implicherebbe il trucco è sicuro.

Una domanda più importante per la ricerca: Qualcuno può trovare una piattaforma / compilatore / linker / loader in cui questo trucco fa non il lavoro? I principali punti brownie per quello. Scommetto che ci sono alcune incorporati processori / sistemi che non piace. Tuttavia, per il desktop computing (e probabilmente mobile / tablet), questo trucco probabilmente funziona ancora.

Il punto non è se gli è possibile. La soluzione banale è

void my_callback_function(struct my_struct* arg);
void my_callback_helper(void* pv)
{
    my_callback_function((struct my_struct*)pv);
}
do_stuff(&my_callback_helper);

Un buon compilatore solo generare il codice per my_callback_helper se è veramente necessario, in tal caso saresti contento che ha fatto.

Si dispone di un tipo di funzione compatibili se i tipi tipo di ritorno e parametri sono compatibili - in fondo (è più complicato, in realtà :)). La compatibilità è lo stesso di "stesso tipo" solo più lassista per consentire di avere tipi diversi, ma hanno ancora una qualche forma di dire "questi tipi sono quasi la stessa". In C89, per esempio, due struct erano compatibili se fossero altrimenti identici ma solo il loro nome era diverso. C99 sembrano aver cambiato. Citando dal c logica documento (lettura altamente raccomandato, btw!):

  

dichiarazioni Struttura, l'unione, o tipo enumerazione in due diverse unità di traduzione non dichiarano formalmente lo stesso tipo, anche se il testo di queste dichiarazioni provengono dallo stesso file di inclusione, dal momento che le stesse unità di traduzione sono disgiunti. La norma specifica così regole di compatibilità aggiuntive per tali tipi, in modo che, se due tali dichiarazioni siano sufficientemente simili sono compatibili.

Detto questo - sì rigorosamente questo è un comportamento indefinito, in quanto la funzione do_stuff o qualcun altro chiamerà la funzione con un puntatore a funzione avendo void* come parametro, ma la funzione ha un parametro incompatibile. Ma comunque, mi aspetto che tutti i compilatori per compilare ed eseguire senza lamenti. Ma si può fare più pulito di avere un'altra funzione prendendo un void* (e la registrazione che, come funzione di callback) che sarà solo chiamare la funzione di vero e proprio allora.

come codice C compilato per l'istruzione, che non si preoccupano affatto di tipi di puntatore, è abbastanza bene di utilizzare il codice si parla. Faresti esegue in problemi quando ci si esegue do_stuff con la funzione di callback e il puntatore a qualcos'altro struttura poi my_struct come argomento.

Spero di riuscire a rendere più chiaro, mostrando ciò che non avrebbe funzionato:

int my_number = 14;
do_stuff((void (*)(void*)) &my_callback_function, &my_number);
// my_callback_function will try to access int as struct my_struct
// and go nuts

o ...

void another_callback_function(struct my_struct* arg, int arg2) { something }
do_stuff((void (*)(void*)) &another_callback_function, NULL);
// another_callback_function will look for non-existing second argument
// on the stack and go nuts

In sostanza, si può lanciare puntatori per quello che vuoi, a patto che i dati continuano a dare un senso a run-time.

Se si pensa al modo in funzione di chiamate di lavoro in C / C ++, spingono alcune voci sullo stack, passa alla nuova posizione di codice, eseguire, quindi pop lo stack al ritorno. Se i puntatori a funzione descrivono le funzioni con lo stesso tipo di ritorno e lo stesso numero / dimensioni di argomenti, si dovrebbe essere a posto.

Quindi, penso che si dovrebbe essere in grado di farlo in modo sicuro.

puntatori void sono compatibili con altri tipi di puntatore. E 'la spina dorsale di come malloc e le funzioni MEM (memcpy, memcmp) lavoro. Tipicamente, in C (Invece di C ++) NULL è una macro definita come ((void *)0).

Look at 6.3.2.3 (punto 1) in C99:

  

Un puntatore a void può essere convertito o da un puntatore a qualsiasi tipo incompleto o oggetto

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top