Undefinierter Verweis auf statisches constexpr char []

184

Ich möchte ein static const charArray in meiner Klasse haben. GCC beschwerte sich und sagte mir, ich solle es verwenden constexpr, obwohl es mir jetzt sagt, dass es eine undefinierte Referenz ist. Wenn ich das Array zu einem Nichtmitglied mache, wird es kompiliert. Was ist los?

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

// .cpp
void foo::bar() {
  std::string str(baz); // undefined reference to baz
}
Pubby
quelle
1
Nur eine Vermutung, funktioniert es, wenn baz zum Beispiel int ist? Können Sie dann darauf zugreifen? Es könnte auch ein Fehler sein.
FailedDev
1
@Pubby: Frage: In welcher Übersetzungseinheit wird sie definiert? Antwort: Alles, was den Header enthält. Problem: Verstößt gegen die One-Definition-Regel. Ausnahme: Konstante Integrale zur Kompilierungszeit können in Headern "initialisiert" werden.
Mooing Duck
Es wird gut als int@MooingDuck kompiliert. Es funktioniert gut als Nichtmitglied. Würde das nicht auch gegen die Regel verstoßen?
Pubby
@ Pubby8: ints betrügen. Als Nichtmitglied sollte dies nicht erlaubt sein, es sei denn, die Regeln für C ++ 11 wurden geändert (möglich)
Mooing Duck
In Anbetracht der Ansichten und Gegenstimmen erforderte diese Frage eine detailliertere Antwort, die ich unten hinzufügte.
Shafik Yaghmour

Antworten:

187

Fügen Sie Ihrer CPP-Datei hinzu:

constexpr char foo::baz[];

Grund: Sie müssen die Definition des statischen Elements sowie die Deklaration angeben. Die Deklaration und der Initialisierer befinden sich in der Klassendefinition, die Elementdefinition muss jedoch getrennt sein.

Kerrek SB
quelle
70
Das sieht komisch aus ... da es dem Compiler keine Informationen zu liefern scheint, die es vorher nicht hatte ...
Reben
32
Es sieht noch seltsamer aus, wenn Sie Ihre Klassendeklaration in einer CPP-Datei haben! Sie initialisieren das Feld in der Klassendeklaration, müssen das Feld jedoch noch " deklarieren ", indem Sie constexpr char foo :: baz [] unter die Klasse schreiben. Es scheint, dass Programmierer, die constexpr verwenden, ihre Programme kompilieren können, indem sie einem seltsamen Tipp folgen: Deklarieren Sie es erneut.
Lukasz Czerwinski
5
@ LukasasCzerwinski: Das Wort, das Sie suchen, ist "definieren".
Kerrek SB
5
Richtig, keine neuen Informationen: Deklarieren Sie mitdecltype(foo::baz) constexpr foo::baz;
not-a-user
6
Wie wird der Ausdruck aussehen, wenn foo als Vorlage verwendet wird? Vielen Dank.
Hei
80

C ++ 17 führt Inline-Variablen ein

C ++ 17 behebt dieses Problem für constexpr staticMitgliedsvariablen, die eine Out-of-Line-Definition erfordern, wenn sie odr-verwendet wurden. In der zweiten Hälfte dieser Antwort finden Sie Details vor C ++ 17.

Vorschlag P0386 Inline-Variablen bietet die Möglichkeit, den inlineSpezifizierer auf Variablen anzuwenden . Insbesondere in diesem Fall constexprimpliziert inlinefür statische Elementvariablen. Der Vorschlag lautet:

Der Inline-Bezeichner kann sowohl auf Variablen als auch auf Funktionen angewendet werden. Eine inline deklarierte Variable hat dieselbe Semantik wie eine inline deklarierte Funktion: Sie kann identisch in mehreren Übersetzungseinheiten definiert werden, muss in jeder Übersetzungseinheit definiert werden, in der sie odr verwendet wird, und das Verhalten des Programms ist wie folgt Es gibt genau eine Variable.

und modifiziert [basic.def] p2:

Eine Erklärung ist eine Definition, es sei denn
...

  • Es deklariert ein statisches Datenelement außerhalb einer Klassendefinition und die Variable wurde innerhalb der Klasse mit dem constexpr-Spezifizierer definiert (diese Verwendung ist veraltet; siehe [depl.static_constexpr]).

...

und füge [depl.static_constexpr] hinzu :

Aus Gründen der Kompatibilität mit früheren internationalen C ++ - Standards kann ein statisches Datenelement von constexpr ohne Initialisierer außerhalb der Klasse redundant deklariert werden. Diese Verwendung ist veraltet. [Beispiel:

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

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

 - Beispiel beenden]


C ++ 14 und früher

In C ++ 03 durften wir nur klasseninterne Initialisierer für const-Integrale oder const-Aufzählungstypen bereitstellen. In C ++ 11 wurde constexprdiese Verwendung auf Literal-Typen erweitert .

In C ++ 11 müssen wir keine Namespace-Bereichsdefinition für ein statisches constexprMitglied angeben , wenn es nicht von odr verwendet wird. Dies können Sie dem Entwurf des C ++ 11-Standardabschnitts 9.4.2 [class.static.data] entnehmen, in dem es heißt ( Hervorhebung meiner in Zukunft ):

[...] Ein statisches Datenelement vom Typ Literal kann in der Klassendefinition mit dem Bezeichner constexpr deklariert werden. In diesem Fall muss in der Deklaration ein Klammer-oder-Gleich-Initialisierer angegeben werden, in dem jede Initialisierer-Klausel, die ein Zuweisungsausdruck ist, ein konstanter Ausdruck ist. [Hinweis: In beiden Fällen kann das Mitglied in konstanten Ausdrücken erscheinen. —End note] Das Mitglied muss weiterhin in einem Namespace-Bereich definiert werden, wenn es im Programm odr-verwendet wird (3.2) und die Definition des Namespace-Bereichs keinen Initialisierer enthält.

Dann stellt sich die Frage, baz ob hier etwas verwendet wird :

std::string str(baz); 

Die Antwort lautet " Ja" . Daher benötigen wir auch eine Definition des Namespace-Bereichs.

Wie bestimmen wir also, ob eine Variable odr-verwendet wird ? Der ursprüngliche C ++ 11-Wortlaut in Abschnitt 3.2 [basic.def.odr] lautet:

Ein Ausdruck wird möglicherweise ausgewertet, es sei denn, es handelt sich um einen nicht bewerteten Operanden (Abschnitt 5) oder einen Unterausdruck davon. Eine Variable, deren Name als potenziell ausgewerteter Ausdruck angezeigt wird, wird odr verwendet, es sei denn, es handelt sich um ein Objekt, das die Anforderungen für das Erscheinen in einem konstanten Ausdruck (5.19) erfüllt, und die Konvertierung von Wert zu Wert (4.1) wird sofort angewendet .

So bazhat sich eine Ausbeute konstanten Ausdruck aber die L - Wert-zu-R - Wert - Umwandlung nicht sofort angewendet , da es nicht anwendbar ist auf Grund bazist ein Array. Dies wird in Abschnitt 4.1 [conv.lval] behandelt, der besagt:

Ein gl-Wert (3.10) eines nicht funktionierenden Nicht-Array-Typs T kann in einen pr-Wert konvertiert werden.53 [...]

Was in der angelegten Array-zu-Zeiger Umwandlung .

Dieser Wortlaut von [basic.def.odr] wurde aufgrund des Fehlerberichts 712 geändert, da einige Fälle nicht von diesem Wortlaut abgedeckt wurden, diese Änderungen jedoch die Ergebnisse für diesen Fall nicht ändern.

Shafik Yaghmour
quelle
Ist uns also klar, dass constexprdas absolut nichts damit zu tun hat? ( bazist sowieso ein konstanter Ausdruck)
MM
@MattMcNabb gut constexpr ist erforderlich, wenn das Mitglied kein Mitglied ist, integral or enumeration typeaber ansonsten ist es wichtig, dass es ein konstanter Ausdruck ist .
Shafik Yaghmour
Im ersten Absatz sollte "ord-used" als "odr-used" lauten, glaube ich, aber ich bin mir mit C ++
Egor Pasko
37

Dies ist wirklich ein Fehler in C ++ 11 - wie andere erklärt haben, hat in C ++ 11 eine statische constexpr-Mitgliedsvariable im Gegensatz zu jeder anderen Art von constexpr-globaler Variable eine externe Verknüpfung und muss daher irgendwo explizit definiert werden.

Es ist auch erwähnenswert, dass Sie in der Praxis beim Kompilieren mit Optimierung häufig mit statischen constexpr-Mitgliedsvariablen ohne Definitionen davonkommen können, da diese bei allen Verwendungszwecken inline sein können. Wenn Sie jedoch ohne Optimierung kompilieren, kann Ihr Programm häufig keine Verknüpfung herstellen. Dies macht dies zu einer sehr häufigen versteckten Falle - Ihr Programm lässt sich gut mit der Optimierung kompilieren, aber sobald Sie die Optimierung deaktivieren (möglicherweise zum Debuggen), kann keine Verknüpfung hergestellt werden.

Gute Nachrichten - dieser Fehler wurde in C ++ 17 behoben! Der Ansatz ist allerdings etwas kompliziert: In C ++ 17 sind statische constexpr-Mitgliedsvariablen implizit inline . Die Inline-Anwendung auf Variablen ist ein neues Konzept in C ++ 17, bedeutet jedoch effektiv, dass sie nirgendwo eine explizite Definition benötigen.

SethML
quelle
4
Up für C ++ 17 info. Sie können diese Informationen zur akzeptierten Antwort hinzufügen!
SR
5

Ist die elegantere Lösung nicht das Ändern char[]in:

static constexpr char * baz = "quz";

Auf diese Weise können wir die Definition / Deklaration / Initialisierer in einer Codezeile haben.

deddebme
quelle
9
Mit können char[]Sie sizeofdie Länge des Strings zur Kompilierungszeit abrufen, mit können char *Sie nicht (es wird die Breite des Zeigertyps zurückgegeben, in diesem Fall 1).
Gnzlbg
2
Dies generiert auch eine Warnung, wenn Sie mit ISO C ++ 11 streng umgehen möchten.
Shital Shah
Siehe meine Antwort, die das sizeofProblem nicht aufweist und in "Nur-Header" -Lösungen verwendet werden kann
Josh Greifer
4

Meine Problemumgehung für die externe Verknüpfung statischer constexprElemente besteht darin, Referenzelement-Getter zu verwenden (was nicht auf das Problem @gnzlbg stößt, das als Kommentar zur Antwort von @deddebme ausgelöst wurde).
Diese Redewendung ist mir wichtig, weil ich es hasse, mehrere CPP-Dateien in meinen Projekten zu haben, und versuche, die Anzahl auf eins zu beschränken, die nur aus #includes und einer main()Funktion besteht.

// 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
quelle
-1

In meiner Umgebung ist gcc vesion 5.4.0. Durch Hinzufügen von "-O2" kann dieser Kompilierungsfehler behoben werden. Es scheint, dass gcc diesen Fall behandeln kann, wenn es um Optimierung geht.

Haishan Zhou
quelle