const statique vs #define

212

Vaut-il mieux utiliser static constvars que #definepréprocesseur? Ou peut-être que cela dépend du contexte?

Quels sont les avantages / inconvénients de chaque méthode?

Patrice Bernassola
la source
14
Scott Meyers couvre ce sujet très bien et en profondeur. Son article n ° 2 dans "Effective C ++ Third Edition". Deux cas particuliers (1) const statique sont préférés dans une portée de classe pour les constantes spécifiques à la classe; (2) l'espace de noms ou la portée anonyme const est préférable à #define.
Eric
2
Je préfère Enums. Parce qu'il est hybride des deux. N'occupe pas d'espace sauf si vous en créez une variable. Si vous souhaitez simplement l'utiliser comme constante, l'énumération est la meilleure option. Il a une sécurité de type en C / C ++ 11 std et également une constante parfaite. #define est de type dangereux, const prend de la place si le compilateur ne peut pas l'optimiser.
siddhusingh
1
Ma décision d'utiliser #defineou static const(pour les chaînes) est motivée par l' aspect d' initialisation (cela n'a pas été mentionné dans les réponses ci-dessous): si la constante est utilisée uniquement dans une unité de compilation particulière, alors j'y vais static const, sinon j'utilise #define- éviter le fiasco d' initialisation d'ordre statique isocpp.org/wiki/faq/ctors#static-init-order
Martin Dvorak
Si const, constexprou enumou toute variation fonctionne dans votre cas, alors préférez-le à#define
Phil1970
@MartinDvorak " éviter le fiasco d'initialisation d'ordre statique " En quoi est-ce un problème pour les constantes?
curiousguy

Réponses:

139

Personnellement, je déteste le préprocesseur, donc j'irais toujours avec const.

Le principal avantage de a #defineest qu'il ne nécessite pas de mémoire à stocker dans votre programme, car il s'agit simplement de remplacer du texte par une valeur littérale. Il a également l'avantage de ne pas avoir de type, il peut donc être utilisé pour n'importe quelle valeur entière sans générer d'avertissement.

Les avantages des " const" sont qu'ils peuvent être étendus et qu'ils peuvent être utilisés dans des situations où un pointeur vers un objet doit être passé.

Je ne sais pas exactement à quoi tu veux en venir avec la staticpartie " ". Si vous déclarez globalement, je le mettrais dans un espace de noms anonyme au lieu de l'utiliser static. Par exemple

namespace {
   unsigned const seconds_per_minute = 60;
};

int main (int argc; char *argv[]) {
...
}
TED
la source
8
Les constantes de chaîne sont spécifiquement l'une de celles qui pourraient bénéficier d'être #defined, du moins si elles peuvent être utilisées comme "blocs de construction" pour des constantes de chaîne plus grandes. Voir ma réponse pour un exemple.
AnT
62
L' #defineavantage de ne pas utiliser de mémoire est inexact. Le "60" dans l'exemple doit être stocké quelque part, que ce soit static constou #define. En fait, j'ai vu des compilateurs où l'utilisation de #define provoquait une consommation de mémoire massive (en lecture seule), et la constante statique n'utilisait pas de mémoire inutile.
Gilad Naor
3
Un #define est comme si vous l'aviez tapé, donc il ne vient certainement pas de la mémoire.
le révérend
27
@theReverend Les valeurs littérales sont-elles en quelque sorte exemptées de consommer les ressources de la machine? Non, ils peuvent simplement les utiliser de différentes manières, peut-être que cela n'apparaîtra pas sur la pile ou le tas, mais à un moment donné, le programme est chargé en mémoire avec toutes les valeurs compilées.
Sqeaky
13
@ gilad-naor, vous avez raison en général, mais de petits nombres entiers comme 60 peuvent en fait parfois être une sorte d'exception partielle. Certains jeux d'instructions ont la capacité de coder des entiers ou un sous-ensemble d'entiers directement dans le flux d'instructions. Par exemple, les MIP ajoutent immédiatement ( cs.umd.edu/class/sum2003/cmsc311/Notes/Mips/addi.html ). Dans ce genre de cas, on pourrait vraiment dire qu'un entier #defined n'utilise aucun espace car dans le binaire compilé, il occupe quelques bits de rechange dans les instructions qui devaient de toute façon exister.
ahcox
242

Avantages et inconvénients entre #defines, consts et (ce que vous avez oublié) enums, selon l'utilisation:

  1. enums:

    • uniquement possible pour les valeurs entières
    • les problèmes de conflit de portée / identificateur correctement gérés, en particulier dans les classes d'énumération C ++ 11 où les énumérations pour enum class Xsont ambiguës par la portéeX::
    • fortement typé, mais à une taille int suffisamment signée ou non signée sur laquelle vous n'avez aucun contrôle en C ++ 03 (bien que vous puissiez spécifier un champ de bits dans lequel ils devraient être compressés si l'énumération est membre de struct / classe / union), tandis que C ++ 11 par défaut est intmais peut être explicitement défini par le programmeur
    • ne peut pas prendre l'adresse - il n'y en a pas car les valeurs d'énumération sont effectivement substituées en ligne aux points d'utilisation
    • contraintes d'utilisation plus fortes (par exemple, incrémentation - template <typename T> void f(T t) { cout << ++t; }ne compile pas, bien que vous puissiez encapsuler une énumération dans une classe avec un constructeur implicite, un opérateur de transtypage et des opérateurs définis par l'utilisateur)
    • chaque type de constante est tiré de l'énumération englobante, donc template <typename T> void f(T)obtenez une instanciation distincte lorsque vous passez la même valeur numérique à partir d'énumérations différentes, qui sont toutes distinctes de toute f(int)instanciation réelle . Le code objet de chaque fonction pourrait être identique (en ignorant les décalages d'adresse), mais je ne m'attendrais pas à ce qu'un compilateur / éditeur de liens élimine les copies inutiles, bien que vous puissiez vérifier votre compilateur / éditeur de liens si vous vous en souciez.
    • même avec typeof / decltype, ne peut pas s'attendre à ce que numeric_limits fournisse un aperçu utile de l'ensemble de valeurs et de combinaisons significatives (en effet, les combinaisons "légales" ne sont même pas notées dans le code source, considérez enum { A = 1, B = 2 }- est A|B"légal" à partir d'une logique de programme la perspective?)
    • le nom de type de l'énumération peut apparaître à divers endroits dans RTTI, les messages du compilateur, etc. - éventuellement utile, éventuellement obscurci
    • vous ne pouvez pas utiliser une énumération sans que l'unité de traduction ne voit réellement la valeur, ce qui signifie que les énumérations dans les API de bibliothèque ont besoin des valeurs exposées dans l'en-tête, makeet d'autres outils de recompilation basés sur l'horodatage déclencheront la recompilation du client lorsqu'ils seront modifiés (mauvais! )

  1. consts:

    • les problèmes de conflit de portée / identificateur correctement gérés
    • type fort, unique et spécifié par l'utilisateur
      • vous pouvez essayer de "taper" un #defineala #define S std::string("abc"), mais la constante évite la construction répétée de temporels distincts à chaque point d'utilisation
    • Complications d'une règle de définition
    • peut prendre l'adresse, leur créer des références const, etc.
    • plus semblable à une non- constvaleur, ce qui minimise le travail et l'impact si la commutation entre les deux
    • la valeur peut être placée à l'intérieur du fichier d'implémentation, permettant une recompilation localisée et juste des liens client pour récupérer la modification

  1. #defines:

    • portée "globale" / plus sujette à des utilisations conflictuelles, qui peuvent produire des problèmes de compilation difficiles à résoudre et des résultats d'exécution inattendus plutôt que des messages d'erreur sensés; atténuer cela nécessite:
      • les identifiants longs, obscurs et / ou coordonnés de manière centrale, et leur accès ne peut pas bénéficier de la correspondance implicite de l'espace de noms utilisé / actuel / de recherche Koenig, des alias d'espace de noms, etc.
      • alors que la meilleure pratique permet aux identificateurs de paramètres de modèle d'être des lettres majuscules à un seul caractère (éventuellement suivies d'un nombre), une autre utilisation des identificateurs sans lettres minuscules est classiquement réservée et attendue des définitions de préprocesseur (en dehors de la bibliothèque OS et C / C ++ en-têtes). Ceci est important pour que l'utilisation du préprocesseur à l'échelle de l'entreprise reste gérable. On peut s'attendre à ce que les bibliothèques tierces se conforment. L'observation de cela implique que la migration des consts ou des énumérations existantes vers / depuis les définitions implique un changement de capitalisation et nécessite donc des modifications du code source du client plutôt qu'une recompilation "simple". (Personnellement, je capitalise la première lettre d'énumérations mais pas les consts, donc je serais également obligé de migrer entre ces deux - peut-être le temps de repenser cela.)
    • plus d'opérations de compilation possibles: concaténation littérale de chaînes, stringification (en prenant sa taille), concaténation en identifiants
      • L'inconvénient est que, compte tenu de l' #define X "x"utilisation de certains clients "pre" X "post", si vous voulez ou devez faire de X une variable modifiable à l'exécution plutôt qu'une constante, vous forcez les modifications du code client (plutôt que la simple recompilation), tandis que cette transition est plus facile à partir d'une const char*ou d' une const std::stringdonnée. oblige déjà l'utilisateur à incorporer des opérations de concaténation (par exemple "pre" + X + "post"pour string)
    • ne peut pas utiliser sizeofdirectement sur un littéral numérique défini
    • non typé (GCC ne prévient pas par rapport à unsigned)
    • certaines chaînes du compilateur / éditeur de liens / débogueur peuvent ne pas présenter l'identifiant, vous serez donc réduit à regarder les "nombres magiques" (chaînes, peu importe ...)
    • ne peut pas prendre l'adresse
    • la valeur substituée n'a pas besoin d'être légale (ou discrète) dans le contexte où le #define est créé, car il est évalué à chaque point d'utilisation, de sorte que vous pouvez référencer des objets non encore déclarés, dépendre de "l'implémentation" qui n'a pas besoin être pré-inclus, créer des "constantes" telles que celles { 1, 2 }qui peuvent être utilisées pour initialiser des tableaux, #define MICROSECONDS *1E-6etc. ( ne le recommande absolument pas!)
    • certaines choses spéciales comme __FILE__et __LINE__peuvent être incorporées dans la macro substitution
    • vous pouvez tester l'existence et la valeur dans les #ifinstructions pour inclure conditionnellement du code (plus puissant qu'un post-prétraitement "si" car le code n'a pas besoin d'être compilable s'il n'est pas sélectionné par le préprocesseur), utilisez #undef-ine, redéfinissez etc.
    • le texte substitué doit être exposé:
      • dans l'unité de traduction utilisée, ce qui signifie que les macros dans les bibliothèques à l'usage du client doivent être dans l'en-tête, makeet d'autres outils de recompilation basés sur l'horodatage déclencheront la recompilation du client lorsqu'ils seront modifiés (mauvais!)
      • ou sur la ligne de commande, où encore plus de soin est nécessaire pour s'assurer que le code client est recompilé (par exemple le Makefile ou le script fournissant la définition doit être répertorié comme une dépendance)

Mon opinion personnelle:

En règle générale, j'utilise consts et les considère comme l'option la plus professionnelle pour un usage général (bien que les autres aient une simplicité attrayante pour cet ancien programmeur paresseux).

Tony Delroy
la source
1
Réponse géniale. Un petit bémol: j'utilise parfois des énumérations locales qui ne sont pas du tout dans les en-têtes juste pour la clarté du code, comme dans les petites machines d'état et autres. Ils n'ont donc pas besoin d'être dans les en-têtes, à tout moment.
kert
Les avantages et les inconvénients sont mélangés, j'aimerais beaucoup voir un tableau comparatif.
Unknown123
@ Unknown123: n'hésitez pas à en poster un - cela ne me dérange pas si vous arrachez des points que vous vous sentez dignes d'ici. Santé
Tony Delroy
48

Si c'est une question C ++ et qu'elle mentionne #definecomme alternative, alors il s'agit de constantes "globales" (c'est-à-dire de portée de fichier), pas de membres de classe. Lorsqu'il s'agit de telles constantes en C ++, static constc'est redondant. En C ++, constles liens internes sont par défaut et il est inutile de les déclarer static. Il est donc vraiment constcontre #define.

Et, enfin, en C ++ constest préférable. Du moins parce que ces constantes sont typées et étendues. Il n'y a tout simplement aucune raison de préférer #defineplus const, à part quelques exceptions.

Les constantes de chaîne, BTW, sont un exemple d'une telle exception. Avec #defineles constantes de chaîne d, on peut utiliser la fonction de concaténation à la compilation des compilateurs C / C ++, comme dans

#define OUT_NAME "output"
#define LOG_EXT ".log"
#define TEXT_EXT ".txt"

const char *const log_file_name = OUT_NAME LOG_EXT;
const char *const text_file_name = OUT_NAME TEXT_EXT;

PS Encore une fois, juste au cas où, lorsque quelqu'un mentionne static constcomme alternative à #define, cela signifie généralement qu'ils parlent de C, pas de C ++. Je me demande si cette question est correctement taguée ...

Fourmi
la source
1
" simplement aucune raison de préférer #define " à quoi? Variables statiques définies dans un fichier d'en-tête?
curiousguy
9

#define peut conduire à des résultats inattendus:

#include <iostream>

#define x 500
#define y x + 5

int z = y * 2;

int main()
{
    std::cout << "y is " << y;
    std::cout << "\nz is " << z;
}

Génère un résultat incorrect:

y is 505
z is 510

Cependant, si vous remplacez cela par des constantes:

#include <iostream>

const int x = 500;
const int y = x + 5;

int z = y * 2;

int main()
{
    std::cout << "y is " << y;
    std::cout << "\nz is " << z;
}

Il produit le résultat correct:

y is 505
z is 1010

En effet, #defineremplace simplement le texte. Parce que cela peut sérieusement perturber l'ordre des opérations, je recommanderais plutôt d'utiliser une variable constante.

Juniorized
la source
1
J'ai eu un résultat inattendu différent: yavait la valeur 5500, une concaténation little-endian de xet 5.
Codes avec Hammer
5

Utiliser une constante statique, c'est comme utiliser n'importe quelle autre variable const dans votre code. Cela signifie que vous pouvez retracer d'où proviennent les informations, par opposition à un #define qui sera simplement remplacé dans le code lors du processus de pré-compilation.

Vous voudrez peut-être jeter un œil à la FAQ C ++ Lite pour cette question: http://www.parashift.com/c++-faq-lite/newbie.html#faq-29.7

Percutio
la source
4
  • Un const statique est tapé (il a un type) et peut être vérifié par le compilateur pour la validité, la redéfinition, etc.
  • un #define peut être redéfini quelque soit indéfini.

Habituellement, vous devriez préférer les constantes statiques. Cela n'a aucun inconvénient. Le préprocesseur devrait être principalement utilisé pour la compilation conditionnelle (et parfois pour les trics vraiment sales peut-être).

RED SOFT ADAIR
la source
3

Il #definen'est pas recommandé de définir les constantes à l'aide de la directive du préprocesseur non seulement dans C++, mais aussi dans C. Ces constantes n'auront pas le type. Même dans a Cété proposé d'utiliser constpour les constantes.


la source
2

Veuillez voir ici: const statique vs définir

généralement une déclaration const (notez qu'elle n'a pas besoin d'être statique) est la voie à suivre

ennuikiller
la source
2

Préférez toujours utiliser les fonctionnalités du langage par rapport à certains outils supplémentaires comme le préprocesseur.

ES.31: N'utilisez pas de macros pour les constantes ou les "fonctions"

Les macros sont une source majeure de bugs. Les macros n'obéissent pas aux règles de portée et de type habituelles. Les macros n'obéissent pas aux règles habituelles de passage d'arguments. Les macros garantissent que le lecteur humain voit quelque chose de différent de ce que voit le compilateur. Les macros compliquent la construction d'outils.

À partir des directives de base C ++

Hitokage
la source
0

Si vous définissez une constante à partager entre toutes les instances de la classe, utilisez const statique. Si la constante est spécifique à chaque instance, utilisez simplement const (mais notez que tous les constructeurs de la classe doivent initialiser cette variable membre const dans la liste d'initialisation).

snr
la source