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

28

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?

Yury Shkliaryk
la source
2
Puisque vous parlez de développement, je crois que vous devez faire tout ce que vous trouvez facile pour changer et expérimenter différentes implémentations. Cela ressemble plus à des préférences personnelles pendant le développement, pas à une meilleure pratique pour résoudre un problème spécifique.
Emerson Cardoso
1
Je recommanderais d'utiliser le modèle de stratégie ou un bon polymorphisme, car cela aide à garder un seul point de plug-in pour le comportement commutable.
pmf
4
Gardez à l'esprit que certains IDE n'évaluent rien dans les #ifdefblocs si le bloc est désactivé. Nous avons rencontré des cas où le code peut facilement devenir obsolète et ne pas être compilé si vous ne créez pas systématiquement tous les chemins.
Berin Loritsch
Jetez un œil à cette réponse que j'ai donnée à une autre question. Il présente quelques façons de rendre beaucoup #ifdefsmoins encombrant.
user1118321

Réponses:

9

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.

Karl Bielefeldt
la source
gitest particulièrement habile à ce genre de chose. Peut-être pas tellement le cas avec svn, hgou d'autres, mais cela peut toujours être fait.
twalberg
C'était aussi ma pensée initiale. "Vous voulez jouer avec quelque chose de différent?" git branch!
Wes Toleman
42

Lorsque vous tenez un marteau, tout ressemble à un clou. Il est tentant une fois que vous savez comment #ifdeffonctionne de 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 était déjà utilisé #ifdefpour 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 des #ifdefsections 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 l' enfer d'entretien de toute façon parce que le programme ne se comportait toujours 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 qui a #ifdeffourni 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.

Neil
la source
Qu'en est-il des versions de débogage, comme "si déboguer, définir la variable x ..." cela semble être utile pour des choses comme la journalisation, mais cela pourrait également changer totalement le fonctionnement de votre programme lorsque le débogage est activé et quand il ne l'est pas .
2017 à 14h34
8
@snb j'y ai pensé. Je préfère toujours pouvoir modifier un fichier de configuration et le rendre plus détaillé. Sinon, quelque chose ne va pas avec le programme en production et vous n'avez aucun moyen de le déboguer sans remplacer complètement l'exécutable. Même dans des situations idéales, cela n'est pas souhaitable. ;)
Neil
Oh ouais, ce serait bien plus idéal de ne pas avoir à recompiler pour déboguer, je n'y ai pas pensé!
2017
9
Pour un exemple extrême de ce que vous décrivez, regardez le 2ème paragraphe sous le sous-titre "Le problème de maintenabilité" dans cet article sur pourquoi MS a atteint le point où ils ont dû réécrire la plupart de leur exécution C à partir de zéro il y a quelques années . blogs.msdn.microsoft.com/vcblog/2014/06/10/…
Dan Neely
2
@snb La plupart des bibliothèques de journalisation supposent un mécanisme de niveau de journalisation. Si vous souhaitez que certaines informations soient enregistrées pendant le débogage, vous les enregistrez avec un faible niveau de journalisation ("Debug" ou "Verbose" généralement). Ensuite, l'application dispose d'un paramètre de configuration qui lui indique le niveau à enregistrer. La réponse reste donc la configuration de ce problème. Cela présente également l'énorme avantage de pouvoir activer ce faible niveau de journalisation dans un environnement client .
jpmc26
21

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 pas plus frustrant que de perdre mon temps à lire la même chose implémentée de quatre manières différentes, seulement pour savoir laquelle 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.

Erdrik Ironrose
la source
1

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 pour 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.

Pieter B
la source