Genericità vs tipo di sicurezza? Utilizzando void * in C
-
19-09-2019 - |
Domanda
Per chi proviene da OO (C #, Java, Scala) apprezzo molto altamente i principi di entrambi riutilizzo del codice e il tipo di sicurezza. Digitare argomenti nelle lingue di cui sopra fare il lavoro e consentire strutture di dati generici che sono sia di tipo-sicuri e non il codice 'rifiuti'.
Come vengo bloccato in C, io sono consapevole che devo fare un compromesso e mi piacerebbe che fosse quella giusta. In entrambi i miei strutture di dati hanno un void *
in ogni nodo / elemento e ho perso la sicurezza di tipo o devo ri-scrivere le mie strutture e codice per ogni tipo che voglio usarli con.
La complessità del codice è un fattore evidente: iterazione attraverso un array o una lista collegata è banale e aggiungendo un *next
ad una struct è sforzo supplementare; in questi casi non ha senso cercare di strutture e riutilizzo del codice. Ma per le strutture più complesse la risposta non è così ovvia.
C'è anche modularità e controllabilità: separare il tipo e le sue operazioni dal codice che utilizza la struttura rende il test più facile. L'inverso è anche vero:. Testare l'iterazione del codice su una struttura, mentre si sta cercando di fare altre cose diventa disordinato
Allora, qual è il tuo consiglio? void *
e il riutilizzo o il tipo di sicurezza e duplicati codice? Ci sono dei principi generali? Sto cercando di forzare OO su procedurale quando non si adatta?
Modifica :! Si prega di non raccomandare C ++, la mia domanda è di circa C
Soluzione
Direi uso void *
in modo da poter riutilizzare il codice. E 'più di lavoro di re-implementare per esempio una lista collegata, che per essere sicuri di ottenere / impostare i dati nella lista in modo appropriato.
Prendere come molti suggerimenti da glib possibile, trovo le loro strutture di dati molto piacevole e facile da usare, e hanno avuto pochi problemi a causa della perdita di sicurezza tipo.
Altri suggerimenti
Credo che si dovrà trovare un equilibrio tra i due, proprio come lei suggerisce. Se il codice è solo poche righe e banale avrei duplicarlo, ma se è più complesso, vorrei prendere in considerazione lavorare con void*
per evitare di dover fare qualsiasi potenziale di fissaggio e manutenzione bug in più punti e anche per ridurre la dimensione del codice.
Se si guarda alla libreria di runtime C, c'è diverse funzioni "generici" che lavorano con void*
, un esempio comune è l'ordinamento con qsort
. Sarebbe follia duplicare il codice per ogni tipo che desideri ordinare.
Non c'è niente di sbagliato con l'utilizzo di puntatori void. Non hanno nemmeno bisogno di lanciare loro quando assegnandoli a una variabile di tipo di puntatore in quanto la conversione è fatta internamente. Si migtht essere la pena di avere uno sguardo a questo: http: //www.cpax. org.uk/prg/writings/casting.php
La risposta a questa domanda è lo stesso di ottenere modelli efficienti per elenco di link in C ++.
a) creare una versione astratta del algoritmo che utilizza void * o qualche Sottratto tipo
b) Creazione di un'interfaccia pubblica leggero per chiamare gli algoritmi di tipo astratto e di casta tra di loro.
Ad esempio.
typedef struct simple_list
{
struct simple_list* next;
} SimpleList;
void add_to_list( SimpleList* listTop, SimpleList* element );
SimpleList* get_from_top( SimpleList* listTop );
// the rest
#define ListType(x) \
void add_ ## x ( x* l, x* e ) \
{ add_to_list( (SimpleList*)l, (SimpleList*)x ); } \
void get_ ## x ( x* l, x* e ) \
{ return (x*) get_from_to( (SimpleList*)l ); } \
/* the rest */
typedef struct my_struct
{
struct my_struct* next;
/* rest of my stuff */
} MyStruct;
ListType(MyStruct)
MyStruct a;
MyStruct b;
add_MyStruct( &a, &b );
MyStruct* c = get_MyStruct(&a);
ecc ecc.
Usiamo OO in C molto qui, ma solo per l'incapsulamento e astrazione, senza il polimorfismo o giù di lì.
Il che significa che abbiamo tipi specifici, come FooBar (Foo una, ...), ma, per la nostra collezione "classi", usiamo void *. Basta usare void * in cui potrebbero essere utilizzati più tipi, ma, così facendo, assicurarsi che non è necessario l'argomento di essere di un tipo specifico. Come da collezione, avendo * vuoto è a posto, perché la raccolta non si preoccupa il tipo. Ma se la funzione può accettare di tipo A e tipo B, ma nessun altro, fare due varianti, una per una e una per b.
Il punto principale è quello di utilizzare un void * solo se non vi interessa circa il tipo.
Ora, se si dispone di 50 tipi con la struttura stessa base (diciamo, int a, int b; come primi membri di tutti i tipi), e vogliono una funzione di agire su quei tipi, basta fare i primi membri comuni di tipo di per sé, quindi effettuare la funzione accettare questo, e passare object-> ab o (AB *) oggetto è il tipo è opaco, entrambi funziona se ab è il primo campo nella struct.
È possibile utilizzare le macro, che funziona con qualsiasi tipo e il compilatore controllerà staticamente il codice espanso. Il rovescio della medaglia è che la densità di codice (in binario) destinata a peggiorare e sono più difficili da eseguire il debug.
Ho fatto questa domanda su funzioni generiche po 'di tempo fa e le risposte potrebbero aiutare.
È possibile aggiungere in modo efficiente tipo di informazione, ereditarietà e polimorfismo di strutture di dati C, questo è quello che fa C ++. ( http://www.embedded.com/97/fe29712.htm )
void*
Sicuramente generica, non duplicare il codice!
Si tenga conto del fatto che questo dilemma è stato considerato da molti un programmatore C, e molti grandi progetti C. Tutti i progetti C gravi che abbia mai incontrato, sia open source o commerciale, scelto il void*
generico. Quando viene utilizzato con cura e avvolto in una buona API, è appena un onere per l'utente della biblioteca. Inoltre, è void*
idiomatica C, consigliato direttamente in K e R2. E 'il modo in cui la gente si aspetta di codice da scrivere, e qualsiasi altra cosa sarebbe sorprendente e male accettato.
È possibile costruire una (specie di) quadro OO in C, ma perdere un sacco di benefici ... come un sistema di tipo OO che il compilatore capisce. Se ti ostini a fare OO in un linguaggio simile al C, C ++ è una scelta migliore. E 'più complicato di vaniglia C, ma almeno si ottiene un adeguato supporto linguistico per OO.
EDIT: Ok ... se insistete che noi sconsigliamo C ++, vi consiglio di non fare OO in C. Felice? Per quanto riguarda le vostre abitudini OO sono interessati, si dovrebbe probabilmente pensare in termini di "oggetti", ma lasciare ereditarietà e polimorfismo fuori della vostra strategia di implementazione. Genericità (usando puntatori a funzione) dovrebbe essere usato con parsimonia.
EDIT 2: In realtà, penso che l'uso di void *
in un elenco C generica è ragionevole. Si sta solo cercando di costruire un quadro OO finta utilizzando le macro, puntatori a funzione, dispacciamento e quel tipo di sciocchezze che secondo me è una cattiva idea.
In Java tutte le collezioni di pacchetto java.util
a effetto stiva equivalente di puntatore void*
(la Object
).
Sì, farmaci generici (introdotto nel 1.5) aggiungere lo zucchero sintattico e vi impedisce di codifica assegnazioni non sicuri, ma il tipo di archiviazione rimane Object
.
Quindi, penso che non c'è crimine OO commesso quando si utilizza void*
per generico tipo di quadro.
Vorrei anche aggiungere inlines tipo-specifici o involucri macro che assegnano / recuperano i dati dalle strutture generiche se fate questo spesso nel codice.
P.S. L'unica cosa che non si deve fare è usare void**
per tornare assegnate tipi generici / riassegnati. Se si controlla le firme di malloc/realloc
si vedrà che si può ottenere allocazioni di memoria corretto senza puntatore void**
temuta. Sto solo dicendo questo perché ho visto questo in qualche progetto open-source, che non voglio nominare qui.
Un contenitore generico può essere spostato con un po 'di lavoro in modo che possa essere istanziato nelle versioni type-safe. Ecco un esempio, intestazione completa legati seguito:
/ * generico implementazione * /
struct deque *deque_next(struct deque *dq);
void *deque_value(const struct deque *dq);
/* Prepend a node carrying `value` to the deque `dq` which may
* be NULL, in which case a new deque is created.
* O(1)
*/
void deque_prepend(struct deque **dq, void *value);
Dal intestazione che può essere utilizzato per creare un'istanza specifici tipi di avvolto deque
#include "deque.h"
#ifndef DEQUE_TAG
#error "Must define DEQUE_TAG to use this header file"
#ifndef DEQUE_VALUE_TYPE
#error "Must define DEQUE_VALUE_TYPE to use this header file"
#endif
#else
#define DEQUE_GEN_PASTE_(x,y) x ## y
#define DEQUE_GEN_PASTE(x,y) DEQUE_GEN_PASTE_(x,y)
#define DQTAG(suffix) DEQUE_GEN_PASTE(DEQUE_TAG,suffix)
#define DQVALUE DEQUE_VALUE_TYPE
#define DQREF DQTAG(_ref_t)
typedef struct {
deque_t *dq;
} DQREF;
static inline DQREF DQTAG(_next) (DQREF ref) {
return (DQREF){deque_next(ref.dq)};
}
static inline DQVALUE DQTAG(_value) (DQREF ref) {
return deque_value(ref.dq);
}
static inline void DQTAG(_prepend) (DQREF *ref, DQVALUE val) {
deque_prepend(&ref->dq, val);
}
- deque.h: http://ideone.com/eDNBN
- deque_gen.h: http://ideone.com/IkJRq