2009-05-14 8 views

risposta

11

Generalmente, n.

Varargs lancia un sacco di sicurezza del tipo: è possibile passare puntatori, float, ecc. Anziché inte e verrà compilato senza problemi. L'uso improprio di vararg, come l'omissione di argomenti, può introdurre arresti anomali a causa di danneggiamento dello stack o lettura di puntatori non validi.

Per esempio, la seguente chiamata verrà compilato e causare crash o altri comportamenti strano:

UpdateField(6, "Field1", 7, "Field2", "Foo"); 

L'iniziale 6 è il numero di parametri da aspettarsi. Converte il puntatore a stringa "Foo" in un int da inserire in Field2, e proverà a leggere e interpretare altri due parametri che non sono presenti, il che probabilmente causerà un crash qui dal rumore dello stack di dereferenziamento.

Credo che l'implementazione di vararg in C sia un errore (dato l'ambiente di oggi - probabilmente aveva perfettamente senso nel 1972.) L'implementazione è che passi un mucchio di valori nello stack e poi il callee porterà il picking allo stack su parametri, in base alla sua interpretazione di alcuni parametri di controllo iniziali. Questo tipo di implementazione, in pratica, ti urla di commettere un errore in quello che potrebbe essere un modo molto difficile da diagnosticare. L'implementazione di C# di questo, passando una serie di oggetti con un attributo sul metodo, è semplicemente necessaria, anche se non direttamente mappabile nel linguaggio C.

+3

quindi passare una grande stringa in un formato noto. Le stringhe sono più facili da serializzare e molto portabili. Devi solo definire un formato per passarli in - XMl è uno (anche se pesante), o farebbe semplicemente un semplice CSV. – gbjbaanb

+1

+1 a gbjbaanb - guarda a (x) printf - la più bella implementazione di varags conosciuta :) – KevinDTimm

5

Un problema con varargs in C è che non si sa quanti argomenti sono passati, quindi è necessario che come un altro parametro:

update(2, FIELD_NAME1, 10, FIELD_NAME2, 20); 

update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30); 
+1

Perché -1 questo? È una risposta totalmente valida (e corretta) alla domanda. L'OP non ha modo di sapere quanti argomenti saranno passati ... E questo è un ottimo esempio di problemi con l'uso di varargs. –

+0

Passare un NULL come ultimo parametro è probabilmente un modo migliore 'update (FIELD_NAME1, 10, FIELD_NAME2, 20, NULL);' in quanto non sarà necessario modificare il parametro firs per aggiungere un altro campo –

+0

@ MustafaSerdarŞanlı - Si presume che ' NULL' è un valido 'FIELD_NAME'. Prevedo che i 'FIELD_NAME' siano valori' enum', quindi avresti bisogno di un valore 'END_FIELD' speciale. L'uso di 'NULL' è buono quando si adatta, ma non sempre si adatta. –

2

mi piacerebbe prendere un sguardo lungo e duro in qualunque "aggiornamento "funzionalità progettata per essere utilizzata esternamente (o anche internamente) che utilizza la stessa funzione per aggiornare molti campi diversi in una struttura. C'è un motivo specifico per cui non è possibile avere funzionalità discrete per l'aggiornamento dei campi?

3

Perché non disporre di un arg, un array. Ancora meglio, un puntatore a un array.

struct field { 
    int val; 
    char* name; 
}; 

o addirittura ...

union datatype { 
    int a; 
    char b; 
    double c; 
    float f; 
// etc; 
}; 

poi

struct field { 
    datatype val; 
    char* name; 
}; 

union (struct* field_name_val_pairs, int len); 

ok 2 args. Ho mentito e ho pensato che un param di lunghezza sarebbe stato buono.

+0

Penso che questo sia un buon progetto, tranne che eviterei il sindacato (basta dare un valore fisso a val). In questo modo, il compilatore può aiutarti a rafforzare la sicurezza del tipo. Certo, la lunghezza deve essere ancora corretta. –

+0

Questo è buono, e update_ary (int n, char ** nomi, datatype * vals) è possibile se un po 'imbarazzante, a volte. – dmckee

+0

L'uso di array al posto di vararg può essere una buona decisione, ma può anche diventare davvero sgradevole quando il tipo di valori è variabile. Pensa al tuo codice pieno di variabili 'void **' ... – brandizzi

6

Tendo ad evitare i vararg, tranne in una circostanza specifica in cui è molto utile. Le argomentazioni variabili non offrono davvero tanto beneficio oltre a quello che può essere fatto dalle singole chiamate di funzione, specialmente nel tuo caso.

In termini di leggibilità (e di solito ciò che preferisco rispetto alla velocità non elaborata salvo casi molto specifici), non vi è alcuna differenza tra le seguenti due opzioni (ho aggiunto un conteggio alle versioni varargs poiché è necessario un contare o sentinella per rilevare la fine dei dati):

update(2, FIELD_NAME1, 10, FIELD_NAME2, 20); 
update(3, FIELD_NAME3, 10, FIELD_NAME4, 20, FIELD_NAME5, 30); 
/* ========== */ 
update(FIELD_NAME1, 10); 
update(FIELD_NAME2, 20); 
update(FIELD_NAME3, 10); 
update(FIELD_NAME4, 20); 
update(FIELD_NAME5, 30); 

infatti, come la versione varargs si allunga, è necessario dividerlo in ogni caso, per la formattazione:

update(5, 
    FIELD_NAME1, 10, 
    FIELD_NAME2, 20, 
    FIELD_NAME3, 10, 
    FIELD_NAME4, 20, 
    FIELD_NAME5, 30); 

Doing il modo "una chiamata per nome di campo" risulta in un codice più semplice i n la funzione stessa e non compromette la leggibilità delle chiamate. Inoltre, consente al compilatore di rilevare correttamente alcuni errori che non è in grado di fare per vararg, come tipi errati o una mancata corrispondenza tra il conteggio fornito dall'utente e il conteggio effettivo.

Se davvero necessario essere in grado di chiamare una singola funzione per farlo, mi piacerebbe optare per:

void update (char *k1, int v1) { 
    ... 
} 
void update2 (char *k1, int v1, char *k2, int v2) { 
    update (k1, v1); 
    update (k2, v2); 
} 
void update3 (char *k1, int v1, char *k2, int v2, char *k3, int v3) { 
    update (k1, v1); /* or these two could be a single */ 
    update (k2, v2); /* update2 (k1, v1, k2, v2); */ 
    update (k3, v3); 
} 
/* and so on. */ 

Si potrebbe anche fare le funzioni di livello superiore, come le macro, se si preferisce, senza perdere la sicurezza del tipo.

L'unico posto in cui tendo ad usare le funzioni varargs è quando si fornisce la stessa funzionalità di printf() - ad esempio, ho dovuto occasionalmente scrivere librerie di logging con funzioni come logPrintf() che forniscono la stessa funzionalità. Non riesco a pensare a qualsiasi altra nel mio lungo (e intendo, tempo lungo :-) al carbone che ho avuto bisogno di usarlo.

Per inciso, se decidi di utilizzare vararg, tendo ad occuparmi delle sentinelle anziché dei conteggi poiché ciò impedisce la mancata corrispondenza durante l'aggiunta dei campi. Si potrebbe facilmente dimenticare di regolare il conteggio e finire con:

update (2, k1, v1, k2, v2, k3, v3); 

quando si aggiunge, che è insidioso perché salta in silenzio k3/v3, oppure:

update (3, k1, v1, k2, v2); 

durante l'eliminazione, che è quasi certamente fatale per il buon funzionamento del tuo programma.

Avere una sentinella impedisce questo (a patto che non si dimentichi la sentinella, ovviamente):

update (k1, v1, k2, v2, k3, v3, NULL); 
1

Un sacco di gente qui hanno suggerito che passa il # dei parametri, tuttavia altri giustamente notare che questo porta a insidiosi bug in cui il # dei campi cambia ma il conteggio passato alla funzione vararg no. Ho risolvere questo in un prodotto utilizzando terminazione null al suo posto:

send_info(INFO_NUMBER, 
      Some_Field,  23, 
      Some_other_Field, "more data", 
      NULL); 

questo modo, quando copia e incolla programmatori inevitabilmente copiarlo, non sono suscettibili di rovinare. E, cosa più importante, non ho intenzione di rovinare tutto.

Guardando indietro al problema originale, si ha una funzione che deve aggiornare una struttura con molti campi e la struttura crescerà.Il metodo usuale (nelle API classiche Win32 e MacOS) di passare dati di questo tipo a una funzione è passare in un'altra struttura (può anche essere la stessa struttura di quello che stai aggiornando), ad esempio:

vuoto aggiornamento (UPDATESTRUCTURE * update_info);

di usarlo, si dovrebbe compilare i campi:

UPDATESTRUCTURE my_update = { 
    UPDATESTRUCTURE_V1, 
    field_1, 
    field_2 
}; 
update(&my_update); 

Più tardi quando si aggiungono nuovi campi, è possibile aggiornare la definizione UPDATESTRUCTURE e ricompilare. Inserendo la versione #, è possibile supportare codice meno recente che non utilizza i nuovi campi.

Una variante del tema è di avere un valore per i campi che non si desidera aggiornare, come KEEP_OLD_VALUE (idealmente questo sarà 0) o NULL.

UPDATESTRUCTURE my_update = { 
    field_1, 
    NULL, 
    field_3 
}; 
update(&my_update); 

non includo una versione perché io approfitto del fatto quando aumentiamo il # di campi in UPDATESTRUCTURE, i campi aggiuntivi viene inizializzata a 0, o KEEP_OLD_VALUE.

+0

Un simile design non è super sicuro e i tuoi esempi contengono potenziali errori. Poiché NULL potrebbe essere definito come solo "0" che è di tipo int, e la dimensione di int potrebbe essere diversa dalla dimensione dei puntatori, potrebbe esserci una mancata corrispondenza. Pertanto * SEMPRE * lanciato su un puntatore per argomenti NULL. Vedi http://www.lysator.liu.se/c/c-faq/c-1.html. – hlovdal

+0

Questo è un buon punto: nell'esempio vararg, non specifico i tipi di Some_Field e Some_other_Field. Se non vengono implementati come numeri interi, sarebbe saggio eseguire il cast del NULL sullo stesso tipo. – Paul

2

Le ragioni fornite finora per evitare i vararg sono tutte buone. Lasciatemi aggiungere un altro che non è stato ancora dato, in quanto è meno importante, ma può essere incontrato. Il vararg impone che il parametro venga passato sullo stack, rallentando così la chiamata della funzione. Su alcune architetture la differenza può essere significativa. Su x86 non è molto importante a causa della sua mancanza di registro, su SPARC, ad esempio, può essere importante. Sui registri sono passati fino a 5 parametri e se la tua funzione utilizza pochi locali, non viene effettuata alcuna regolazione dello stack. Se la tua funzione è una funzione foglia (cioè non chiama un'altra funzione), non c'è anche la regolazione della finestra. Il costo della chiamata è quindi molto piccolo. Con un vararg, viene eseguita la normale sequenza dei parametri di passaggio nello stack, la regolazione dello stack e la gestione delle finestre oppure la funzione non sarà in grado di ottenere i parametri. Ciò aumenta significativamente il costo della chiamata.

Problemi correlati