Constantes globales en C++11

Quelles sont les meilleures façons de déclarer et définir les constantes globales en C++? Je suis surtout intéressé par la norme C++11 car elle corrige beaucoup à cet égard.

[EDIT (clarification)]: dans cette question, "constante globale" indique une variable constante ou une fonction qui est connue au moment de la compilation dans champ d'application. La constante globale doit être accessible depuis plus d'une unité de traduction. Il n'est pas nécessairement constante de type constexpr-peut être quelque chose comme const std::map<int, std::string> m = { { 1, "U" }, { 5, "V" } }; ou const std::map<int, std::string> * mAddr() { return & m; }. Je ne touche pas préférable bonne portée ou le nom de constante dans cette question. Laissons ces questions pour une autre question. [END_EDIT]

je veux des réponses pour tous les différents cas, donc supposons que T est une des options suivantes:

typedef    int                     T;  // 1
typedef    long double             T;  // 2
typedef    std::array<char, 1>     T;  // 3
typedef    std::array<long, 1000>  T;  // 4
typedef    std::string             T;  // 5
typedef    QString                 T;  // 6
class      T {
   // unspecified amount of code
};                                     // 7
// Something special
// not mentioned above?                // 8

je crois qu'il n'y a pas de grand sémantique (je ne discute pas bon de nommer ou de la portée de style ici) de différence entre les 3 scopes:

// header.hpp
extern const T tv;
T tf();                  // Global
namespace Nm {
    extern const T tv;
    T tf();              // Namespace
}
struct Cl {
    static const T tv;
    static T tf();       // Class
};

mais si vous choisissez meilleure façon de variantes ci-dessous dépend de la différence entre la déclaration ci-dessus étendues, veuillez l'indiquer.

considérer aussi le cas où l'appel de fonction est utilisé en définition constante, par exemple <some value>==f();. Comment l'appel d'une fonction dans une initialisation constante influencerait-il le choix entre des alternatives?

  1. considérons Tconstexpr constructeur en premier. Solutions de rechange évidentes sont:

    // header.hpp
    namespace Ns {
    constexpr T A = <some value>;
    constexpr T B() { return <some value>; }
    inline const T & C() { static constexpr T t = <some value>; return t; }
    const T & D();
    }
    
    // source.cpp
    const T & Ns::D() { static constexpr T t = <some value>; return t; }
    

    je pense que A et B conviennent le mieux aux petits T (de sorte que le fait d'avoir plusieurs instances ou de le copier à l'exécution ne pose pas de problème), par exemple 1-3 parfois 7. C et D mieux si T est grande, par exemple,4 parfois 7.

  2. T sans constexpr constructeur. Alternatives:

    // header.hpp
    namespace Ns {
    extern const T a;
    inline T b() { return <some value>; }
    inline const T & c() { static const T t = <some value>; return t; }
    const T & d();
    }
    
    // source.cpp
    extern const T Ns::a = <some value>;
    const T & Ns::d() { static const T t = <some value>; return t; }
    

    je ne serait pas normalement utiliser a à cause du fiasco de l'ordre d'initialisation statique. Autant que je sache, b, c et d sont parfaitement sûrs, même thread-safe depuis C++11. b ne semble pas être un bon choix, sauf si T a un constructeur très bon marché, ce qui est rare pour les constructeurs non-constexpr. Je peux en citer un avantage de cd - pas d'appel de fonction (au moment de l'exécution de la performance); un avantage de dc - moins de recompilation lorsque la valeur de constant est changée (ces avantages s'appliquent aussi à C et D). Je suis sûr que j'ai raté beaucoup de ici le raisonnement. Veuillez fournir d'autres considérations dans vos réponses.

Si vous souhaitez modifier / tester le code ci-dessus, vous pouvez utiliser mes fichiers de test (juste en-tête.php, source.cpp avec des versions compilables des fragments de code ci-dessus et main.rpc qui imprime des constantes à partir de l'en-tête.hpp): https://docs.google.com/uc?export=download&id=0B0F-aqLyFk_PVUtSRnZWWnd4Tjg

17
demandé sur vedg 2014-05-14 16:56:27

3 réponses

je crois qu'il n'y a pas de grande différence entre la déclaration suivante endroits:

C'est mal à bien des égards.

la première déclaration pollue le namespace global; vous avez pris le nom "tv" de jamais être utilisé à nouveau sans la possibilité de malentendus. Cela peut causer des avertissements d'ombre, cela peut causer des erreurs de linker, cela peut causer toutes sortes de confusion à quiconque utilise votre en-tête. Il peut aussi causer des problèmes de quelqu'un qui n'utilise pas votre en-tête, en provoquant une collision avec quelqu'un d'autre qui arrive aussi d'utiliser votre nom de variable comme globale.

une telle approche n'est pas recommandée en C++ moderne, mais est omniprésente en C, et conduit donc à beaucoup d'utilisation du mot-clé statique pour les variables "globales" en a.c fichier de fichier (champ d'application).

le second déclare pollue un namespace; c'est beaucoup moins un problème, car les namespaces sont librement renamables et peuvent être faits gratuitement. Aussi longtemps que deux les projets utilisent leur propre espace de nom relativement spécifique, aucune collision ne se produira. Dans le cas où de telles collisions se produisent, les espaces de noms de chacune peuvent être renommés pour éviter tout problème.

C'est plus moderne, c++03 style, et C++11 étend cette tactique considérablement avec le changement de nom des gabarits.

la troisième approche est une structure, pas une classe; ils ont des différences, surtout si vous voulez maintenir la compatibilité avec C. Les avantages d'un composé de portée de classe sur le namespace scope; non seulement vous pouvez facilement encapsuler plusieurs choses et utiliser un nom spécifique, vous pouvez également augmenter l'encapsulation via les méthodes et la dissimulation de l'information, ce qui augmente considérablement l'utilité de votre code. C'est surtout l'avantage des classes, indépendamment de la portée des avantages.

Vous ne devriez presque certainement pas utiliser le premier, à moins que vos fonctions et variables soient très larges et STL/STD comme, ou votre programme est très petit et peu susceptible d'être intégré ou réutiliser.

regardons maintenant vos cas.

  1. La taille du constructeur, si elle renvoie une expression constante, est sans importance; tout le code doit être exécutable au moment de la compilation. Cela signifie que la complexité n'est pas significative; elle sera toujours compilée à une valeur unique, constante, de retour. Vous ne devez presque certainement jamais utiliser C ou D; tout ce qui fait est de faire les optimisations de constexpr ne fonctionne pas. J'utiliserais celui de A et B qui semble le plus élégant, probablement une simple tâche serait A, et une expression constante complexe serait B.

  2. aucun de ceux-ci ne sont nécessairement thread safe; le contenu du constructeur déterminerait la sécurité du thread et de l'exception, et il est assez facile de rendre l'un de ces énoncés non thread safe. En fait, A est le plus susceptible d'être thread safe; aussi longtemps que l'objet n'est pas accédé jusqu'à ce que main est appelé, il devrait être entièrement formé; la même chose ne peut pas être dit de l'un de vos autres exemple. En ce qui concerne votre analyse de B, d'après mon expérience, la plupart des constructeurs (surtout ceux qui sont sûrs à l'exception) sont bon marché car ils évitent l'allocation. Dans de tels cas, il est peu probable qu'il y ait une grande différence entre vos cas.

je vous recommande fortement de vous arrêter d'essayer de micro-optimisations comme cela et peut-être obtenir une solide compréhension de C++ les expressions idiomatiques. La plupart des choses que vous essayez de faire ici sont entraînera pas une augmentation de la performance.

7
répondu Alice 2014-05-14 17:43:32

Vous n'avez pas mentionné une option importante:

namespace
{
    const T t = .....;
};

maintenant il n'y a pas de problèmes de collision de noms.

Ce n'est pas approprié, si T est quelque chose que vous ne voulez construire une fois. Mais avoir un grand objet "global", const ou pas, est quelque chose que vous voulez vraiment éviter. Il casse l'encapsulation, et introduit également le fiasco de l'ordre d'initialisation statique dans votre code.

Je n'ai jamais eu besoin d'un grand objet externe. Si j'ai besoin d'un grand par exemple, j'écris une fonction (peut-être en tant que membre d'une classe) qui regarde vers le haut de la table; et la table est locale à l'unité avec la mise en œuvre de cette fonction.

Dans mon code qui semble appeler à un grand non-const objet global, en fait, j'ai une fonction,

namespace MyStuff
{
     T &get_global_T();
}

qui construit l'objet lors de la première utilisation. (En fait, l'objet lui-même est caché dans une unité, et T est une classe helper qui spécifie une interface; donc je peux Messer autour avec les détails de l'objet et ne pas déranger tout code qui l'utilise).

2
répondu M.M 2014-05-15 21:45:22

1

au cas où A il y a une différence entre la portée globale ou l'espace de noms (Lien interne) et la portée de classe (lien externe). Donc

// header.hpp
constexpr T A = <some value>; // internal linkage
namespace Nm { constexpr T A = <some value>; } // internal linkage
class Cl { public: static constexpr T A = <some value>; }; // not enough!

Envisager l'utilisation suivante:

// user.cpp
std::cout << A << Nm::A << Cl::A; // ok
std::cout << &A << &Nm::A;        // ok
std::cout << &Cl::A;              // linker error: undefined reference to `Cl::A'

placement Cl::A définition source.rpc (en plus de ce qui précède Cl::A déclaration) élimine cette erreur:

// source.cpp
constexpr T Cl::A;

les liens externes signifient qu'il n'y a toujours qu'une seule instance de Cl::A. Donc Cl::A semble être un très bon candidat pour le grand T. Cependant: peut-on être sûr que le fiasco de l'ordre d'initialisation statique ne se présenterait pas dans ce cas? Je crois que la réponse est oui, parce que Cl::A est construit au moment de la compilation.

j'ai testé A,B,a alternatives avec g++ 4.8.2 et 4.9.0, clang++ 3.4 sur la plate-forme GNU/Linux. Les résultats pour trois translations unités:

  • A dans la portée de la classe avec la définition dans source.rpc était à la fois insensible au fiasco et avait la même adresse dans toutes les unités de traduction, même au moment de la compilation.
  • A dans namespace ou global scope avait 3 adresses différentes à la fois pour les grands tableaux et constexpr const char * A = "A"; (en raison de liaison interne).
  • B (std::array<long double, 100>) dans n'importe quelle portée avait 2 adresses différentes (l'adresse était la même dans 2 unités de traduction)); en outre, tous les 3 B les adresses suggéraient un emplacement de mémoire différent (elles étaient beaucoup plus grandes que les autres adresses) - je soupçonne que le tableau a été copié en mémoire à l'exécution.
  • a lorsqu'il est utilisé avec constexpr types T, e.g. int,const char *,std::array, et initialisé avec constexpr expression source.rpc, a été aussi bon que A: abri fiasco et a la même adresse dans toutes les unités de traduction. Si la constante de constexpr tapez T est initialisé avec non -constexpr, e.g. std::time(nullptr), et utilisé avant l'initialisation, il contient la valeur par défaut (par exemple, 0int). Cela signifie que la valeur de constante peut dépendre de l'ordre d'initialisation statique dans ce cas. Donc, ne initialiser a avec non -constexpr valeur!

le bas de La ligne

  1. préférez A dans la portée de classe pour n'importe quelle constante de constexpr dans la plupart des cas parce qu'il allie sécurité parfaite, simplicité, économie de mémoire et performance.
  2. a (initialisé avec constexpr valeur source.rpc