Perché i puntatori di funzione e i puntatori di dati non sono compatibili in C / C ++?

Ho letto che la conversione di un puntatore di funzione in un puntatore di dati e viceversa funziona sulla maggior parte delle piattaforms, ma non è garantito il funzionamento. Perché è così? Non dovrebbero entrambi essere semplicemente indirizzi nella memoria principale e quindi essere compatibili?

Un’architettura non deve memorizzare codice e dati nella stessa memoria. Con un’architettura di Harvard, codice e dati sono archiviati in una memoria completamente diversa. La maggior parte delle architetture sono architetture di Von Neumann con codice e dati nella stessa memoria ma C non si limita solo a certi tipi di architetture, se ansible.

Alcuni computer hanno (avuto) spazi di indirizzi separati per codice e dati. Su tale hardware non funziona.

Il linguaggio è progettato non solo per le attuali applicazioni desktop, ma per consentirne l’implementazione su un ampio set di hardware.


Sembra che il comitato linguistico C non abbia mai inteso il void* come un puntatore alla funzione, volevano semplicemente un puntatore generico agli oggetti.

La motivazione C99 dice:

6.3.2.3 Puntatori
C è ora implementato su una vasta gamma di architetture. Mentre alcune di queste architetture presentano puntatori uniformi che hanno le dimensioni di un certo tipo intero, il codice massimamente portatile non può assumere alcuna corrispondenza necessaria tra diversi tipi di puntatore e tipi interi. In alcune implementazioni, i puntatori possono persino essere più ampi di qualsiasi tipo intero.

L’uso di void* (“pointer to void “) come un tipo di puntatore di object generico è un’invenzione del Comitato C89. L’adozione di questo tipo è stata stimolata dal desiderio di specificare gli argomenti del prototipo di funzione che convertono tranquillamente i puntatori arbitrari (come in fread ) o si lamentano se il tipo di argomento non corrisponde esattamente (come in strcmp ). Nulla viene detto sui puntatori alle funzioni, che possono essere incommensurabili con puntatori di oggetti e / o interi.

Nota Nulla viene detto sui puntatori alle funzioni nell’ultimo paragrafo. Potrebbero essere diversi dagli altri indicatori, e il comitato ne è consapevole.

Per coloro che ricordano MS-DOS, Windows 3.1 e precedenti, la risposta è abbastanza semplice. Tutti questi elementi erano utilizzati per supportare diversi modelli di memoria, con diverse combinazioni di caratteristiche per il codice e i puntatori di dati.

Ad esempio, per il modello Compact (codice piccolo, dati grandi):

 sizeof(void *) > sizeof(void(*)()) 

e viceversa nel modello Medium (codice grande, piccoli dati):

 sizeof(void *) < sizeof(void(*)()) 

In questo caso non disponevi di spazio di archiviazione separato per codice e data, ma non riuscivo ancora a convertire tra i due puntatori (con l'uso di modificatori __near e __far non standard).

Inoltre, non c'è alcuna garanzia che anche se i puntatori hanno le stesse dimensioni, che puntino alla stessa cosa - nel modello DOS Small memory, sia il codice che i dati usati vicino ai puntatori, ma hanno indicato segmenti diversi. Pertanto, la conversione di un puntatore a un puntatore di dati non fornisce un puntatore che ha alcuna relazione con la funzione e pertanto non è stato utilizzato per tale conversione.

I puntatori a vuoto dovrebbero essere in grado di ospitare un puntatore a qualsiasi tipo di dati, ma non necessariamente un puntatore a una funzione. Alcuni sistemi hanno requisiti diversi per i puntatori alle funzioni rispetto ai puntatori ai dati (ad esempio, esistono DSP con indirizzamento diverso per dati e codice, modello medio su puntatori a 32 bit utilizzati da MS-DOS per il codice ma solo indicatori a 16 bit per i dati) .

Oltre a quanto già detto, è interessante dare un’occhiata a POSIX dlsym() :

Lo standard ISO C non richiede che i puntatori alle funzioni possano essere inoltrati avanti e indietro ai puntatori ai dati. In effetti, lo standard ISO C non richiede che un object di tipo void * possa contenere un puntatore a una funzione. Le implementazioni che supportano l’estensione XSI, tuttavia, richiedono che un object di tipo void * possa contenere un puntatore a una funzione. Tuttavia, il risultato della conversione di un puntatore in una funzione in un puntatore a un altro tipo di dati (ad eccezione di void *) non è ancora definito. Si noti che i compilatori conformi allo standard ISO C sono necessari per generare un avviso se si tenta una conversione da un puntatore void * a un puntatore di funzione come in:

  fptr = (int (*)(int))dlsym(handle, "my_function"); 

A causa del problema notato qui, una versione futura può aggiungere una nuova funzione per restituire i puntatori di funzione, oppure l’interfaccia corrente può essere deprecata a favore di due nuove funzioni: una che restituisce i puntatori di dati e l’altra che restituisce i puntatori di funzione.

C ++ 11 ha una soluzione alla mancata corrispondenza di dlsym() data tra C / C ++ e POSIX per quanto riguarda dlsym() . È ansible utilizzare reinterpret_cast per convertire un puntatore a / da un puntatore dati finché l’implementazione supporta questa funzionalità.

Dallo standard, 5.2.10 par. 8, “la conversione di un puntatore di funzione in un tipo di puntatore a object o viceversa è supportata in modo condizionale.” 1.3.5 definisce “supportato in modo condizionale” come un “costrutto di programma che un’implementazione non è richiesta per supportare”.

A seconda dell’architettura di destinazione, codice e dati possono essere memorizzati in aree di memoria fondamentalmente incompatibili e fisicamente distinte.

non definito non significa necessariamente non consentito, può significare che l’implementatore del compilatore ha più libertà di farlo come vogliono.

Ad esempio, potrebbe non essere ansible su alcune architetture – undefined consente loro di avere ancora una libreria ‘C’ conforms anche se non è ansible farlo.

Possono essere diversi tipi con diversi requisiti di spazio. Assegnare a uno può dividere in modo irreversibile il valore del puntatore in modo che l’assegnazione di risultati restituiti in qualcosa di diverso.

Credo che possano essere di tipo diverso perché lo standard non vuole limitare le possibili implementazioni che salvano spazio quando non è necessario o quando le dimensioni potrebbero far sì che la CPU debba fare delle cagate extra per usarlo, ecc …

Un’altra soluzione:

Supponendo che POSIX garantisca che le funzioni e i puntatori di dati abbiano la stessa dimensione e rappresentazione (non riesco a trovare il testo per questo, ma l’esempio OP citato suggerisce che almeno intendessero fare questo requisito), il seguente dovrebbe funzionare:

 double (*cosine)(double); void *tmp; handle = dlopen("libm.so", RTLD_LAZY); tmp = dlsym(handle, "cos"); memcpy(&cosine, &tmp, sizeof cosine); 

Ciò evita di violare le regole di aliasing passando attraverso la rappresentazione char [] , che è consentita per l’alias di tutti i tipi.

Ancora un altro approccio:

 union { double (*fptr)(double); void *dptr; } u; u.dptr = dlsym(handle, "cos"); cosine = u.fptr; 

Ma raccomanderei l’approccio memcpy se vuoi assolutamente il 100% corretto di C.

L’unica soluzione veramente portatile non è usare dlsym per le funzioni, e invece usare dlsym per ottenere un puntatore ai dati che contengono i puntatori di funzione. Ad esempio, nella tua libreria:

 struct module foo_module = { .create = create_func, .destroy = destroy_func, .write = write_func, /* ... */ }; 

e poi nella tua domanda:

 struct module *foo = dlsym(handle, "foo_module"); foo->create(/*...*/); /* ... */ 

Per inciso, questa è comunque una buona pratica di progettazione, e rende facile supportare sia il caricamento dinamico tramite dlopen e il collegamento statico di tutti i moduli su sistemi che non supportano il collegamento dinamico, o dove l’utente / system integrator non vuole utilizzare il collegamento dinamico.

Sulla maggior parte delle architetture, i puntatori a tutti i tipi di dati normali hanno la stessa rappresentazione, quindi il cast tra i tipi di puntatore dati è un no-op.

Tuttavia, è ipotizzabile che i puntatori di funzione possano richiedere una rappresentazione diversa, forse sono più grandi di altri indicatori. Se void * potrebbe contenere i puntatori di funzione, ciò significherebbe che la rappresentazione di void * dovrebbe essere la dimensione più grande. E tutti i cast di puntatori di dati verso / da void * dovrebbero eseguire questa copia extra.

Come qualcuno ha detto, se hai bisogno di questo puoi ottenerlo usando un sindacato. Ma la maggior parte degli usi di void * sono solo per dati, quindi sarebbe oneroso aumentare l’uso della memoria nel caso in cui sia necessario memorizzare un puntatore a funzione.

Un moderno esempio di dove i puntatori di funzione possono differire in termini di dimensioni dai puntatori di dati: puntatori di funzioni dei membri della class C ++

Quotato direttamente da https://blogs.msdn.microsoft.com/oldnewthing/20040209-00/?p=40713/

 class Base1 { int b1; void Base1Method(); }; class Base2 { int b2; void Base2Method(); }; class Derived : public Base1, Base2 { int d; void DerivedMethod(); }; 

Ora ci sono due possibili suggerimenti.

Un puntatore a una funzione membro di Base1 può essere utilizzato come puntatore a una funzione membro di Derived , poiché entrambi utilizzano lo stesso puntatore. Ma un puntatore a una funzione membro di Base2 non può essere usato così come un puntatore a una funzione membro di Derived , poiché this puntatore deve essere regolato.

Ci sono molti modi per risolvere questo. Ecco come il compilatore di Visual Studio decide di gestirlo:

Un puntatore a una funzione membro di una class ereditata da più è davvero una struttura.

 [Address of function] [Adjustor] 

La dimensione di una funzione pointer-to-member di una class che utilizza l’ereditarietà multipla è la dimensione di un puntatore più la dimensione di un size_t .

tl; dr: quando si utilizza l’ereditarietà multipla, un puntatore a una funzione membro può essere (a seconda del compilatore, della versione, dell’architettura, ecc.) effettivamente memorizzato come

 struct { void * func; size_t offset; } 

che è ovviamente più grande di un void * .

So che questo non è stato commentato dal 2012, ma ho pensato che sarebbe stato utile aggiungere che conosco un’architettura che ha indicatori molto incompatibili per dati e funzioni dal momento che una chiamata su tale architettura controlla i privilegi e porta informazioni extra. Nessuna quantità di casting aiuterà. È il mulino .