web-dev-qa-db-fra.com

Utilisation de #ifdef pour basculer entre différents types de comportement pendant le développement

Est-ce une bonne pratique d'utiliser #ifdef pendant le développement pour basculer entre différents types de comportement? Par exemple, je veux changer le comportement du code existant, j'ai plusieurs idées pour changer le comportement et il est nécessaire de basculer entre les différentes implémentations pour tester et comparer différentes approches. Les modifications de code sont généralement complexes et influencent différentes méthodes dans différents fichiers.

J'introduis généralement plusieurs identifiants et je fais quelque chose comme ça

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;
}

Cela permet de basculer rapidement entre différentes approches et de tout faire en une seule copie du code source. Est-ce une bonne approche pour le développement ou existe-t-il une meilleure pratique?

28
Yury Shkliaryk

Je préférerais utiliser des branches de contrôle de version pour ce cas d'utilisation. Cela vous permet de faire la différence entre les implémentations, de conserver un historique distinct pour chacune, et lorsque vous avez pris votre décision et que vous devez supprimer l'une des versions, il vous suffit de supprimer cette branche au lieu de passer par une modification sujette aux erreurs.

9
Karl Bielefeldt

Lorsque vous tenez un marteau, tout ressemble à un clou. C'est tentant une fois que vous savez comment #ifdef fonctionne pour l'utiliser comme une sorte de moyen d'obtenir un comportement personnalisé dans votre programme. Je le sais parce que j'ai fait la même erreur.

J'ai hérité d'un programme hérité écrit en MFC C++ qui utilisait déjà #ifdef pour définir des valeurs spécifiques à la plate-forme. Cela signifiait que je pouvais compiler mon programme pour l'utiliser sur une plate-forme 32 bits ou une plate-forme 64 bits simplement en définissant (ou dans certains cas en ne définissant pas) des valeurs de macro spécifiques.

Le problème s'est alors posé que j'avais besoin d'écrire un comportement personnalisé pour un client. J'aurais pu créer une branche et créer une base de code distincte pour le client, mais cela aurait fait un enfer de maintenance. J'aurais également pu définir des valeurs de configuration à lire par le programme au démarrage et utiliser ces valeurs pour déterminer le comportement, mais je devrais ensuite créer des configurations personnalisées pour ajouter les valeurs de configuration appropriées aux fichiers de configuration pour chaque client.

J'ai été tenté et j'ai cédé. J'ai écrit #ifdef sections dans mon code pour différencier les différents comportements. Ne vous y trompez pas, ce n'était rien au-dessus au début. Des changements de comportement très mineurs ont été effectués, ce qui m'a permis de redistribuer des versions du programme à nos clients et je n'ai pas besoin d'avoir plus d'une version de la base de code.

Au fil du temps, cela est devenu un enfer de maintenance de toute façon parce que le programme ne se comportait plus de manière cohérente dans tous les domaines. Si je voulais tester une version du programme, je devais nécessairement savoir qui était le client. Le code, bien que j'aie essayé de le réduire à un ou deux fichiers d'en-tête, était très encombré et l'approche de correction rapide que #ifdef fourni signifiait de telles solutions réparties dans tout le programme comme un cancer malin.

J'ai depuis appris ma leçon, et vous devriez aussi. Utilisez-le si vous en avez absolument besoin et utilisez-le strictement pour les changements de plate-forme. La meilleure façon d'approcher les différences de comportement entre les programmes (et donc les clients) est de modifier uniquement la configuration chargée au démarrage. Le programme reste cohérent et il devient à la fois plus facile à lire et à déboguer.

42
Neil

temporairement il n'y a rien de mal à ce que vous faites (par exemple, avant l'enregistrement): c'est un excellent moyen de tester différentes combinaisons de techniques ou d'ignorer une section de code (bien que cela parle de problèmes en soi).

Mais un mot d'avertissement: ne gardez pas les branches #ifdef il n'y a rien de plus frustrant que de perdre mon temps à lire la même chose mise en œuvre de quatre façons différentes, seulement pour comprendre - lequel je devrais lire.

La lecture d'un #ifdef demande des efforts car vous devez vous souvenir de le sauter! Ne le rendez pas plus difficile qu'il ne doit l'être.

Utilisez #ifdefs aussi modérément que possible. Il existe généralement des moyens de le faire dans votre environnement de développement pour les différences permanentes, telles que les versions Debug/Release, ou pour différentes architectures.

J'ai écrit des fonctionnalités de bibliothèque qui dépendaient des versions de bibliothèque incluses, qui nécessitaient des divisions #ifdef. Donc, parfois, cela peut être le seul moyen, ou le plus simple, mais même alors, vous devriez être contrarié de les garder.

21
Erdrik Ironrose

Utiliser #ifdefs comme ça rend le code très difficile à lire.

Donc, non, n'utilisez pas #ifdefs comme ça.

Il peut y avoir des tonnes d'arguments pourquoi ne pas utiliser ifdefs, pour moi celui-ci est suffisant.

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

Peut faire beaucoup de choses:

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

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

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

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

Tout dépend des approches définies ou non. Ce qu'il fait n'est absolument pas clair à première vue.

1
Pieter B