Utilizzo di #ifdef per alternare i diversi tipi di comportamento durante lo sviluppo


28

È buona norma utilizzare #ifdef durante lo sviluppo per passare da un tipo di comportamento all'altro? Ad esempio, voglio cambiare il comportamento del codice esistente, ho diverse idee su come cambiare il comportamento ed è necessario passare tra diverse implementazioni per testare e confrontare diversi approcci. Di solito le modifiche al codice sono complesse e influenzano metodi diversi in file diversi.

Di solito introduco diversi identificatori e faccio qualcosa del genere

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

void bar()
{
    doSomething3();
#ifndef APPROACH3
    doSomething4();
#endif
    doSomething5();
#ifdef APPROACH2
    bar_approach2();
#endif
}

int main()
{
    foo();
    bar();
    return 0;
}

Ciò consente di passare rapidamente da un approccio all'altro e fare tutto in una sola copia del codice sorgente. È un buon approccio per lo sviluppo o esiste una pratica migliore?



2
Dal momento che stai parlando di sviluppo, credo che devi fare tutto ciò che ritieni facile da fare per passare e sperimentare diverse implementazioni. Questo è più simile alle preferenze personali durante lo sviluppo, non ad alcune migliori pratiche per risolvere un problema specifico.
Emerson Cardoso,

1
Consiglio di utilizzare il modello di strategia o il buon vecchio polimorfismo, poiché questo aiuta a mantenere un unico punto di innesto per il comportamento commutabile.
pmf

4
Tieni presente che alcuni IDE non valutano nulla in #ifdefblocchi se il blocco è disattivato. Ci siamo imbattuti in casi in cui il codice può facilmente diventare obsoleto e non compilare se non si creano abitualmente tutti i percorsi.
Berin Loritsch,

Dai un'occhiata a questa risposta che ho dato a un'altra domanda. Stabilisce alcuni modi per rendere molti #ifdefsmeno ingombranti.
user1118321

Risposte:


9

Preferirei utilizzare i rami di controllo versione per questo caso d'uso. Ciò ti consente di differenziare tra le implementazioni, mantenere una cronologia separata per ciascuna e quando hai preso la tua decisione e devi rimuovere una delle versioni, devi semplicemente scartare quel ramo invece di passare attraverso una modifica soggetta a errori.


gitè particolarmente abile in questo genere di cose. Forse non tanto con svn, hgo con altri, ma può ancora essere fatto.
twalberg,

Anche quello era il mio pensiero iniziale. "Vuoi scherzare con qualcosa di diverso?" git branch!
Wes Toleman,

42

Quando hai in mano un martello, tutto sembra un chiodo. È allettante quando sai come #ifdeffunziona usarlo come una sorta di mezzo per ottenere un comportamento personalizzato nel tuo programma. Lo so perché ho fatto lo stesso errore.

Ho ereditato un programma legacy scritto in M ​​++ C ++ che già utilizzava #ifdefper definire valori specifici della piattaforma. Ciò significava che potevo compilare il mio programma per utilizzarlo su una piattaforma a 32 bit o su una piattaforma a 64 bit semplicemente definendo (o in alcuni casi non definendo) valori macro specifici.

Sorse quindi il problema che avevo bisogno di scrivere un comportamento personalizzato per un client. Avrei potuto creare una filiale e creare una base di codice separata per il client, ma ciò avrebbe reso un inferno di manutenzione. Avrei potuto anche definire i valori di configurazione che il programma doveva leggere all'avvio e utilizzare questi valori per determinare il comportamento, ma avrei quindi dovuto creare impostazioni personalizzate per aggiungere i valori di configurazione corretti ai file di configurazione per ciascun client.

Ero tentato e mi sono arreso. Ho scritto #ifdefsezioni nel mio codice per differenziare i vari comportamenti. Non commettere errori, all'inizio non era niente di esagerato. Sono state apportate modifiche di comportamento molto minori che mi hanno permesso di ridistribuire le versioni del programma ai nostri clienti e non ho bisogno di avere più di una versione della base di codice.

Con il passare del tempo questo è diventato comunque un inferno di manutenzione perché il programma non si è comportato più coerentemente su tutta la linea. Se volevo testare una versione del programma, dovevo necessariamente sapere chi fosse il cliente. Il codice, anche se ho cercato di ridurlo a uno o due file di intestazione, era molto disordinato e l'approccio di correzione rapida che #ifdefforniva significava che tali soluzioni si diffondevano nel programma come un tumore maligno.

Da allora ho imparato la mia lezione e anche tu dovresti. Usalo se assolutamente necessario e usalo rigorosamente per le modifiche alla piattaforma. Il modo migliore per affrontare le differenze di comportamento tra i programmi (e quindi i client) è modificare solo la configurazione caricata all'avvio. Il programma rimane coerente e diventa sia più facile da leggere che da eseguire il debug.


che dire delle versioni di debug, come "se debug, definisci la variabile x ..." sembra che potrebbe essere utile per cose come la registrazione, ma potrebbe anche cambiare totalmente il modo in cui il tuo programma funziona quando il debug è abilitato e quando non lo è .
quando il

8
@snb Ci ho pensato. Preferisco ancora essere in grado di modificare un file di configurazione e farlo registrare con maggiori dettagli. Altrimenti qualcosa va storto con il programma in produzione e non hai modo di eseguire il debug senza sostituire completamente l'eseguibile. Anche in situazioni ideali, questo è meno desiderabile. ;)
Neil,

Oh sì, sarebbe molto più ideale non dover ricompilare il debug, non ci ho pensato!
quando il

9
Per un esempio estremo di ciò che stai descrivendo, guarda il secondo paragrafo sotto il sottotitolo "Il problema della manutenibilità" in questo articolo sul perché gli Stati Uniti hanno raggiunto il punto in cui hanno dovuto riscrivere la maggior parte del loro tempo di esecuzione da qualche anno fa . blogs.msdn.microsoft.com/vcblog/2014/06/10/…
Dan Neely,

2
@snb La maggior parte delle librerie di log suppone un meccanismo a livello di log. Se si desidera che determinate informazioni vengano registrate durante il debug, le si registra con un livello di registrazione basso ("Debug" o "Verbose" in genere). Quindi l'applicazione ha un parametro di configurazione che indica quale livello registrare. Quindi la risposta è ancora la configurazione per questo problema. Questo ha anche l'enorme vantaggio di poter attivare questo livello di registrazione basso in un ambiente client .
jpmc26

21

Temporaneamente non c'è niente di sbagliato in ciò che stai facendo (diciamo, prima del check-in): è un ottimo modo per testare diverse combinazioni di tecniche o ignorare una sezione di codice (anche se parla di problemi in sé e per sé).

Ma un avvertimento: non tenere i rami di #ifdef, c'è poco più frustrante che sprecare il mio tempo a leggere la stessa cosa implementato in quattro modi diversi, solo per capire quale dovrei leggere .

Leggere su un #ifdef richiede uno sforzo, poiché in realtà devi ricordarti di saltarlo! Non renderlo più difficile di quanto debba assolutamente essere.

Usa #ifdefs nel modo più parsimonioso possibile. Esistono generalmente modi in cui è possibile farlo all'interno dell'ambiente di sviluppo per differenze permanenti , come build di debug / release o per architetture diverse.

Ho scritto funzionalità di libreria che dipendevano dalle versioni di libreria incluse, che richiedevano divisioni #ifdef. Quindi a volte potrebbe essere l'unico modo, o il più semplice, ma anche allora dovresti essere arrabbiato per averli tenuti.


1

L'uso di #ifdefs in questo modo rende il codice molto difficile da leggere.

Quindi no, non usare #ifdefs in questo modo.

Potrebbero esserci molte discussioni sul perché non usare ifdefs, per me questo è abbastanza.

void foo()
{
    doSomething1();
#ifdef APPROACH1
    foo_approach1();
#endif
    doSomething2();
#ifdef APPROACH2
    foo_approach2();
#endif
}

Può fare molte cose che può fare:

void foo()
{
    doSomething1();
    doSomething2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
}

void foo()
{
    doSomething1();
    doSomething2();
    foo_approach2();
}

void foo()
{
    doSomething1();
    foo_approach1();
    doSomething2();
    foo_approach2();
}

Tutto a seconda di quali approcci sono definiti o meno. Quello che fa non è assolutamente chiaro al primo sguardo.

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.