Référence non définie à statique constexpr char []

196

Je veux avoir un static const chartableau dans ma classe. GCC s'est plaint et m'a dit que je devrais utiliser constexpr, bien que maintenant il me dise que c'est une référence indéfinie. Si je fais du tableau un non-membre, il se compile. Que se passe-t-il?

// .hpp
struct foo {
  void bar();
  static constexpr char baz[] = "quz";
};

// .cpp
void foo::bar() {
  std::string str(baz); // undefined reference to baz
}
Pubby
la source
1
Juste une intuition, ça marche si baz est int par exemple? Pouvez-vous alors y accéder? Cela pourrait aussi être un bug.
FailedDev
1
@Pubby: Question: Dans quelle unité de traduction sera-t-il défini? Réponse: tout ce qui inclut l'en-tête. Problème: enfreint la règle de définition unique. Exception: les intégrales de constantes au moment de la compilation peuvent être "initialisées" dans les en-têtes.
Mooing Duck
Il se compile bien en tant que int@MooingDuck. Il fonctionne bien en tant que non-membre. Cela ne violerait-il pas la règle aussi?
Pubby
@ Pubby8: inttriche. En tant que non-membre, cela ne devrait pas être autorisé, à moins que les règles ne soient modifiées pour C ++ 11 (possible)
Mooing Duck
Compte tenu des opinions et des votes positifs, cette question nécessitait une réponse plus détaillée, que j'ai ajoutée ci-dessous.
Shafik Yaghmour

Réponses:

202

Ajoutez à votre fichier cpp:

constexpr char foo::baz[];

Raison: vous devez fournir la définition du membre statique ainsi que la déclaration. La déclaration et l'initialiseur vont dans la définition de classe, mais la définition de membre doit être séparée.

Kerrek SB
la source
72
Cela semble étrange ... car il ne semble pas fournir au compilateur des informations qu'il n'avait pas auparavant ...
vines
33
Cela semble encore plus étrange lorsque vous avez votre déclaration de classe dans un fichier .cpp! Vous initialisez le champ dans la déclaration de classe, mais vous devez toujours « déclarer » le champ en écrivant constexpr char foo :: baz [] sous la classe. Il semble que les programmeurs utilisant constexpr puissent compiler leurs programmes en suivant une astuce étrange: le déclarer à nouveau.
Lukasz Czerwinski
5
@LukaszCzerwinski: Le mot que vous recherchez est "définir".
Kerrek SB
5
Bien, pas de nouvelles informations: déclarer en utilisantdecltype(foo::baz) constexpr foo::baz;
not-a-user
6
à quoi ressemblera l'expression si foo est modélisé? Merci.
Hei
85

C ++ 17 introduit des variables en ligne

C ++ 17 corrige ce problème pour constexpr staticles variables membres nécessitant une définition hors ligne si elle était utilisée par odr. Voir la seconde moitié de cette réponse pour les détails pré-C ++ 17.

La proposition P0386 Variables en ligne introduit la possibilité d'appliquer le inlinespécificateur aux variables. En particulier, ce cas constexprimplique inlinepour les variables membres statiques. La proposition dit:

Le spécificateur en ligne peut être appliqué aux variables ainsi qu'aux fonctions. Une variable déclarée en ligne a la même sémantique qu'une fonction déclarée en ligne: elle peut être définie, de manière identique, en plusieurs unités de traduction, doit être définie dans chaque unité de traduction dans laquelle elle est utilisée, et le comportement du programme est comme si il y a exactement une variable.

et modifié [basic.def] p2:

Une déclaration est une définition à moins que
...

  • il déclare un membre de données statique en dehors d'une définition de classe et la variable a été définie dans la classe avec le spécificateur constexpr (cette utilisation est obsolète; voir [depr.static_constexpr]),

...

et ajoutez [depr.static_constexpr] :

Pour la compatibilité avec les normes internationales C ++ antérieures, un membre de données statique constexpr peut être redondant de manière redondante en dehors de la classe sans initialiseur. Cette utilisation est obsolète. [ Exemple:

struct A {
  static constexpr int n = 5;  // definition (declaration in C++ 2014)
};

constexpr int A::n;  // redundant declaration (definition in C++ 2014)

 - fin d'exemple]


C ++ 14 et versions antérieures

En C ++ 03, nous étions uniquement autorisés à fournir des initialiseurs en classe pour les intégrales const ou les types d'énumération const , en C ++ 11, l'utilisation de constexprcela a été étendue aux types littéraux .

En C ++ 11, nous n'avons pas besoin de fournir une définition de portée d'espace de noms pour un constexprmembre statique s'il n'est pas utilisé par odr , nous pouvons le voir dans le projet de section standard de C ++ 11 9.4.2 [class.static.data] qui dit ( je souligne pour l'avenir ):

[...] Un membre de données statique de type littéral peut être déclaré dans la définition de classe avec le spécificateur constexpr; si tel est le cas, sa déclaration doit spécifier un initialiseur d'accolade ou d'égalité dans lequel chaque clause d'initialisation qui est une expression d'affectation est une expression constante. [Remarque: dans ces deux cas, le membre peut apparaître dans des expressions constantes. —End note] Le membre doit toujours être défini dans une portée d'espace de noms s'il est utilisé par odr (3.2) dans le programme et la définition de portée d'espace de noms ne doit pas contenir d'initialiseur.

Alors la question devient, est baz utilisée ici:

std::string str(baz); 

et la réponse est oui , et nous avons donc également besoin d'une définition de la portée de l'espace de noms.

Alors, comment déterminer si une variable est utilisée par odr ? Le libellé original de C ++ 11 dans la section 3.2 [basic.def.odr] dit:

Une expression est potentiellement évaluée à moins qu'il ne s'agisse d'un opérande non évalué (article 5) ou d'une sous-expression de celui-ci. Une variable dont le nom apparaît comme une expression potentiellement évaluée est utilisée par odr à moins qu'il ne s'agisse d'un objet qui satisfait aux exigences pour apparaître dans une expression constante (5.19) et que la conversion lvalue-en-rvalue (4.1) est immédiatement appliquée .

Il en résultebaz une expression constante, mais la conversion de lvaleur en rvalue n'est pas immédiatement appliquée car elle n'est pas applicable car il s'agit d' bazun tableau. Ceci est couvert dans la section 4.1 [conv.lval] qui dit:

Une glvalue (3.10) d'un type T sans fonction et sans tableau peut être convertie en une valeur pr.53 [...]

Ce qui est appliqué dans la conversion tableau-pointeur .

Ce libellé de [basic.def.odr] a été modifié en raison du rapport d'anomalie 712 car certains cas n'étaient pas couverts par ce libellé, mais ces changements ne modifient pas les résultats pour ce cas.

Shafik Yaghmour
la source
alors sommes-nous clairs que cela constexprn'a absolument rien à voir avec cela? ( bazest une expression constante de toute façon)
MM
@MattMcNabb well constexpr est requis si le membre n'est pas un integral or enumeration typemais sinon, oui, ce qui compte, c'est que ce soit une expression constante .
Shafik Yaghmour
Dans le premier paragraphe, "ord-used" devrait se lire comme "odr-used", je crois, mais je ne suis jamais sûr avec C ++
Egor Pasko
40

C'est vraiment une faille dans C ++ 11 - comme d'autres l'ont expliqué, dans C ++ 11, une variable membre statique constexpr, contrairement à tout autre type de variable globale constexpr, a un lien externe et doit donc être explicitement définie quelque part.

Il est également intéressant de noter que vous pouvez souvent vous en sortir avec des variables membres constexpr statiques sans définitions lors de la compilation avec optimisation, car elles peuvent se retrouver en ligne dans toutes les utilisations, mais si vous compilez sans optimisation, votre programme échouera souvent à se lier. Cela en fait un piège caché très courant - votre programme se compile correctement avec l'optimisation, mais dès que vous désactivez l'optimisation (peut-être pour le débogage), il ne parvient pas à se lier.

Bonne nouvelle cependant - cette faille est corrigée dans C ++ 17! L'approche est cependant un peu compliquée: en C ++ 17, les variables membres statiques constexpr sont implicitement en ligne . L' application en ligne aux variables est un nouveau concept en C ++ 17, mais cela signifie effectivement qu'elles n'ont pas besoin d'une définition explicite nulle part.

SethML
la source
4
Pour les informations C ++ 17. Vous pouvez ajouter cette information à la réponse acceptée!
SR
5

La solution la plus élégante n'est-elle pas de changer le char[]en:

static constexpr char * baz = "quz";

De cette façon, nous pouvons avoir la définition / déclaration / initialiseur en 1 ligne de code.

deddebme
la source
10
avec char[]vous pouvez utiliser sizeofpour obtenir la longueur de la chaîne au moment de la compilation, avec char *vous ne pouvez pas (cela retournera la largeur du type de pointeur, 1 dans ce cas).
gnzlbg
3
Cela génère également un avertissement si vous souhaitez être strict avec ISO C ++ 11.
Shital Shah
Voir ma réponse qui ne présente pas le sizeofproblème, et peut être utilisée dans des solutions "en-tête uniquement"
Josh Greifer
Ajoutez const pour corriger l'avertissement ISO: static constexpr const char * baz = "quz";
mentalmushroom
4

Ma solution de contournement pour le lien externe des membres statiques est d'utiliser constexprdes getters de membre de référence (ce qui ne rencontre pas le problème @gnzlbg soulevé en tant que commentaire à la réponse de @deddebme).
Cet idiome est important pour moi car je déteste avoir plusieurs fichiers .cpp dans mes projets, et j'essaie de limiter le nombre à un, qui ne se compose que de #includes et d'une main()fonction.

// foo.hpp
struct foo {
  static constexpr auto& baz() { return "quz"; }
};

// some.cpp

  auto sz = sizeof(foo::baz()); // sz == 4

  auto& foo_baz = foo::baz();  // note auto& not auto
  auto sz2 =  sizeof(foo_baz);    // 4
  auto name = typeid(foo_baz).name();  // something like 'char const[4]'
Josh Greifer
la source
0

Dans mon environnement, gcc vesion est 5.4.0. L'ajout de "-O2" peut corriger cette erreur de compilation. Il semble que gcc puisse gérer ce cas lorsqu'il demande une optimisation.

Haishan Zhou
la source