Utilizzo di #ifdef per passare da diversi tipi di comportamento durante lo sviluppo

27

È buona norma usare #ifdef durante lo sviluppo per passare da diversi tipi di comportamento? Ad esempio, voglio cambiare il comportamento del codice esistente, ho diverse idee su come modificare il comportamento ed è necessario passare da diverse implementazioni per testare e confrontare approcci diversi. Di solito i cambiamenti nel codice sono complessi e influenzano i diversi metodi in diversi file.

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 diversi approcci e fare tutto in un'unica copia del codice sorgente. È un buon approccio per lo sviluppo o esiste una pratica migliore?

    
posta Yury Shkliaryk 20.12.2017 - 12:10
fonte

4 risposte

9

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

    
risposta data 20.12.2017 - 19:57
fonte
42

Quando hai in mano un martello, tutto sembra un chiodo. È allettante una volta che sai come #ifdef funziona per 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 MFC C ++ che già utilizzava #ifdef per definire i valori specifici della piattaforma. Ciò significava che potevo compilare il mio programma da utilizzare su una piattaforma a 32 bit o su una piattaforma a 64 bit semplicemente definendo (o in alcuni casi non definendo) specifici valori macro.

Quindi è sorto 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 cliente, ma ciò avrebbe reso un inferno di manutenzione. Avrei potuto anche definire i valori di configurazione da leggere dal programma all'avvio e utilizzare questi valori per determinare il comportamento, ma dovrei quindi creare configurazioni personalizzate per aggiungere i valori di configurazione corretti ai file di configurazione per ciascun client.

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

Nel corso del tempo questo è diventato un problema di manutenzione comunque perché il programma non si è più comportato in modo coerente su tutta la linea. Se volevo testare una versione del programma, dovevo necessariamente sapere chi era il cliente. Il codice, sebbene provassi a ridurlo a uno o due file di intestazione, era molto disordinato e l'approccio di correzione rapida fornito da #ifdef significava che tali soluzioni si diffondevano in tutto il programma come un cancro maligno.

Da allora ho imparato la lezione e anche tu dovresti. Usalo se devi assolutamente 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 ed entrambi diventano più facili da leggere e da eseguire correttamente.

    
risposta data 20.12.2017 - 12:49
fonte
21

Temporaneamente non c'è nulla di sbagliato in quello che stai facendo (ad esempio prima del check-in): è un ottimo modo per testare diverse combinazioni di tecniche o per ignorare una sezione di codice (sebbene che parla di problemi di per sé).

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

La lettura di un #ifdef richiede uno sforzo, in quanto devi ricordarti di saltarlo! Non renderlo più difficile di quanto debba essere assolutamente.

Usa #ifdefs con la massima parsimonia possibile. Esistono in genere modi per farlo all'interno del tuo ambiente di sviluppo per le differenze permanenti , come le build di Debug / Release o per diverse architetture.

Ho scritto le funzionalità della libreria che dipendevano dalle versioni della libreria incluse, che richiedevano divisioni #ifdef. Quindi a volte può essere l'unico modo, o il più semplice, ma anche in questo caso dovresti essere sconvolto nel mantenerli.

    
risposta data 20.12.2017 - 12:24
fonte
1

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

Quindi, no, non usare #ifdefs in questo modo.

Potrebbero esserci tonnellate di argomenti perché non usare ifdefs, per me questo è sufficiente.

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

Può fare un sacco di 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 dipende da quali approcci sono definiti o meno. Ciò che fa non è assolutamente chiaro al primo sguardo.

    
risposta data 20.12.2017 - 12:18
fonte

Leggi altre domande sui tag