Undefinierter Verweis auf statische const int

79

Ich bin heute auf ein interessantes Problem gestoßen. Betrachten Sie dieses einfache Beispiel:

template <typename T>
void foo(const T & a) { /* code */ }

// This would also fail
// void foo(const int & a) { /* code */ }

class Bar
{
public:
   static const int kConst = 1;
   void func()
   {
      foo(kConst);           // This is the important line
   }
};

int main()
{
   Bar b;
   b.func();
}

Beim Kompilieren erhalte ich eine Fehlermeldung:

Undefined reference to 'Bar::kConst'

Ich bin mir ziemlich sicher, dass dies daran liegt, dass das static const intnirgendwo definiert ist, was beabsichtigt ist, da der Compiler nach meinem Verständnis in der Lage sein sollte, das Ersetzen zur Kompilierungszeit vorzunehmen und keine Definition zu benötigen. Da die Funktion jedoch einen const int &Parameter verwendet, scheint sie die Substitution nicht vorzunehmen und stattdessen eine Referenz zu bevorzugen. Ich kann dieses Problem beheben, indem ich folgende Änderung vornehme:

foo(static_cast<int>(kConst));

Ich glaube, dies zwingt den Compiler jetzt dazu, ein temporäres int zu erstellen und dann einen Verweis auf das zu übergeben, was er zur Kompilierungszeit erfolgreich ausführen kann.

Ich habe mich gefragt, ob dies beabsichtigt war oder erwarte ich zu viel von gcc, um diesen Fall behandeln zu können? Oder sollte ich das aus irgendeinem Grund nicht tun?

JaredC
quelle
1
In der Praxis könnte man einfach const int kConst = 1;das gleiche Ergebnis erzielen. Es gibt auch selten einen Grund (ich kann mir keinen vorstellen), dass eine Funktion einen Parameter des Typs übernimmt const int &- verwenden Sie einfach einen inthier.
Björn Pollex
1
@Space die eigentliche Funktion war eine Vorlage, ich werde meine Frage bearbeiten, um das zu erwähnen.
JaredC
1
@Space fyi, wenn es nicht gemacht wird, staticwird ein Fehler ausgegeben. "ISO C ++ verbietet die Initialisierung des Mitglieds 'kConst' ... macht 'kConst' statisch. '
JaredC
Mein schlechtes, danke für die Korrektur.
Björn Pollex
1
Das ärgerliche ist, dass dieser Fehler bei harmlosen Verwendungen auftreten kann std::min( some_val, kConst), da er std::min<T>Parameter vom Typ hat T const &, und die Implikation ist, dass wir einen Verweis auf die kConst übergeben müssen. Ich fand es nur, wenn die Optimierung ausgeschaltet war. Mit statischer Besetzung behoben.
Greggo

Antworten:

61

Es ist beabsichtigt, 9.4.2 / 4 sagt:

Wenn ein statisches Datenelement vom Typ const Integral oder vom Typ const enumeration ist, kann seine Deklaration in der Klassendefinition einen Konstanteninitialisierer angeben, der ein integraler konstanter Ausdruck sein soll (5.19). In diesem Fall kann das Element in integralen konstanten Ausdrücken erscheinen. Das Mitglied muss weiterhin in einem Namespace-Bereich definiert werden, wenn es im Programm verwendet wird

Wenn Sie das statische Datenelement als const-Referenz übergeben, "verwenden" Sie es, 3.2 / 2:

Ein Ausdruck wird möglicherweise ausgewertet, es sei denn, er erscheint dort, wo ein integraler konstanter Ausdruck erforderlich ist (siehe 5.19), ist der Operand des Operators sizeof (5.3.3) oder der Operand des Operators typeid und der Ausdruck bezeichnet keinen l-Wert von polymorpher Klassentyp (5.2.8). Ein Objekt oder eine nicht überladene Funktion wird verwendet, wenn ihr Name in einem potenziell ausgewerteten Ausdruck erscheint.

Tatsächlich "verwenden" Sie es also, wenn Sie es auch als Wert übergeben, oder in a static_cast. Es ist nur so, dass GCC Sie in einem Fall vom Haken gelassen hat, im anderen nicht.

[Bearbeiten: gcc wendet die Regeln aus C ++ 0x-Entwürfen an: "Eine Variable oder nicht überladene Funktion, 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 Anzeigen in einer Konstanten erfüllt Ausdruck (5.19) und die Umwandlung von lWert in rWert (4.1) werden sofort angewendet. " Die statische Umwandlung führt sofort eine lvalue-rvalue-Konvertierung durch, sodass sie in C ++ 0x nicht "verwendet" wird.]

Das praktische Problem mit der const-Referenz besteht darin, dass sie berechtigt ist, foodie Adresse ihres Arguments zu übernehmen und sie beispielsweise mit der Adresse des Arguments eines anderen Aufrufs zu vergleichen, der in einem globalen Aufruf gespeichert ist. Da ein statisches Datenelement ein eindeutiges Objekt ist, bedeutet dies, dass bei einem Aufruf foo(kConst)von zwei verschiedenen TUs die Adresse des übergebenen Objekts jeweils gleich sein muss. AFAIK GCC kann dies nur arrangieren, wenn das Objekt in einer (und nur einer) TU definiert ist.

OK, in diesem Fall foohandelt es sich also um eine Vorlage, daher ist die Definition in allen TUs sichtbar. Vielleicht könnte der Compiler theoretisch das Risiko ausschließen, dass er irgendetwas mit der Adresse macht. Aber im Allgemeinen sollten Sie sicherlich keine Adressen oder Verweise auf nicht vorhandene Objekte nehmen ;-)

Steve Jessop
quelle
1
Vielen Dank für das Beispiel, die Adresse der Referenz zu übernehmen. Ich denke, das ist ein wirklich praktischer Grund, warum der Compiler nicht das tut, was ich erwarte.
JaredC
Für eine vollständige Konformität müssten Sie wahrscheinlich so etwas wie definieren template <int N> int intvalue() { return N; }. Dann mit intvalue<kConst>, kConsterscheint nur in einem Kontext einen integralen konstanten Ausdruck erfordert, und so wird nicht verwendet. Die Funktion gibt jedoch eine temporäre Datei mit demselben Wert wie zurück kConst, die an eine const-Referenz gebunden werden kann. Ich bin mir jedoch nicht sicher, ob es einen einfacheren Weg zur tragbaren Durchsetzung kConstgibt, der nicht verwendet wird.
Steve Jessop
1
Ich habe das gleiche Problem, wenn ich eine solche statische const-Variable in einem ternären Operator (dh so etwas r = s ? kConst1 : kConst2) mit gcc 4.7 verwende. Ich habe es mit einem tatsächlichen gelöst if. Trotzdem danke für die Antwort!
Clodéric
2
... und std :: min / std :: max, was mich hierher geführt hat!
Salbei
"AFAIK GCC kann das nur arrangieren, wenn das Objekt in einer (und nur einer) TU definiert ist." Es ist zu schade, da dies bereits mit Konstanten möglich ist - kompilieren Sie sie mehrmals als 'schwache Defs' in .rodata und lassen Sie den Linker dann nur eine auswählen -, um sicherzustellen, dass alle tatsächlichen Refs dieselbe Konstante haben. Dies ist eigentlich das, was für typeid getan wird; Es kann jedoch auf seltsame Weise fehlschlagen, wenn gemeinsam genutzte Bibliotheken verwendet werden.
Greggo
27

Wenn Sie eine statische const-Variable mit dem Initialisierer innerhalb der Klassendeklaration schreiben, ist es so, als hätten Sie geschrieben

class Bar
{
      enum { kConst = 1 };
}

und GCC wird es genauso behandeln, was bedeutet, dass es keine Adresse hat.

Der richtige Code sollte sein

class Bar
{
      static const int kConst;
}
const int Bar::kConst = 1;
Pelya
quelle
Vielen Dank für dieses anschauliche Beispiel.
Shuhalo
12

Dies ist ein wirklich gültiger Fall. Vor allem, weil foo eine Funktion aus der STL wie std :: count sein könnte, die ein const T & als drittes Argument verwendet.

Ich habe viel Zeit damit verbracht zu verstehen, warum der Linker Probleme mit einem so grundlegenden Code hatte.

Die Fehlermeldung

Undefinierter Verweis auf 'Bar :: kConst'

sagt uns, dass der Linker kein Symbol finden kann.

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 U Bar::kConst

Wir können am 'U' erkennen, dass Bar :: kConst undefiniert ist. Wenn der Linker versucht, seine Arbeit zu erledigen, muss er daher das Symbol finden. Aber nur du deklarieren jedoch kConst und definieren es nicht.

Die Lösung in C ++ besteht auch darin, es wie folgt zu definieren:

template <typename T>
void foo(const T & a) { /* code */ }

class Bar
{
public:
   static const int kConst = 1;
   void func()
   {
      foo(kConst);           // This is the important line
   }
};

const int Bar::kConst;       // Definition <--FIX

int main()
{
   Bar b;
   b.func();
}

Dann können Sie sehen, dass der Compiler die Definition in die generierte Objektdatei einfügt:

$nm -C main.o
0000000000000000 T main
0000000000000000 W void foo<int>(int const&)
0000000000000000 W Bar::func()
0000000000000000 R Bar::kConst

Jetzt können Sie das 'R' sehen, das besagt, dass es im Datenabschnitt definiert ist.

Stac
quelle
Ist es in Ordnung, dass eine Konstante in der Ausgabe von "nm-C" zweimal erscheint, zuerst mit einem "R" und einer Adresse und dann mit einem "U"?
quant_dev
Hast du ein Beispiel? Auf dem angegebenen Beispiel >nm -C main.o | grep kConstgibt mir nur eine Zeile 0000000000400644 R Bar::kConst.
Stac
Ich sehe es beim Kompilieren einer statischen Bibliothek.
quant_dev
1
In diesem Fall natürlich! Eine statische Bibliothek ist nur ein Aggregat von Objektdateien. Die Verknüpfung erfolgt nur durch den Client der statischen Bibliothek. Wenn Sie also die Archivdatei, die Objektdatei mit der Definition der Konstante und eine andere Objektdatei mit dem Namen Bar :: func () einfügen, sehen Sie das Symbol einmal mit der Definition und einmal ohne: nm -C lib.agibt Ihnen Constants.o: 0000000000000000 R Bar::kConstund main_file.o: U Bar::kConst ....
Stac
2

g ++ Version 4.3.4 akzeptiert diesen Code (siehe diesen Link ). Aber g ++ Version 4.4.0 lehnt es ab.

TonyK
quelle
2

Ich denke, dieses Artefakt von C ++ bedeutet, dass das jederzeit möglich ist Bar::kConst verwiesen wird, stattdessen sein Literalwert verwendet wird.

Dies bedeutet, dass es in der Praxis keine Variable gibt, auf die Bezug genommen werden kann.

Möglicherweise müssen Sie dies tun:

void func()
{
  int k = kConst;
  foo(k);
}
Quamrana
quelle
Dies ist im Grunde das, was ich erreicht habe, indem ich es geändert habe foo(static_cast<int>(kConst));, oder?
JaredC
2

Sie können es auch durch eine constexpr-Mitgliedsfunktion ersetzen:

class Bar
{
  static constexpr int kConst() { return 1; };
};
Yoav
quelle
Beachten Sie, dass dies sowohl 4 Zeilen in der Deklaration als auch geschweifte Klammern nach der "Konstante" erfordert, sodass Sie am Ende foo = std :: numeric_limits <int> :: max () * bar :: this_is_a_constant_that_looks_like_a_method () schreiben und hoffen, dass Ihre Codierungsstandard-Copes und der Optimierer repariert es für Sie.
Code Abominator
1

Einfacher Trick: Verwenden Sie +vor der kConstWeitergabe der Funktion. Dadurch wird verhindert, dass der Konstante eine Referenz entnommen wird. Auf diese Weise generiert der Code keine Linkeranforderung für das Konstantenobjekt, sondern setzt stattdessen den Konstantenwert für die Compilerzeit fort.

Ethouris
quelle
Schade, dass der Compiler keine Warnung ausgibt, wenn eine Adresse von einem static constWert übernommen wird, der bei der Deklaration initialisiert wird. Dies würde immer zu einem Linkerfehler führen, und wenn dieselbe Konstante auch in einer Objektdatei separat deklariert wird, wäre dies ebenfalls ein Fehler. Der Compiler ist sich auch der Situation voll bewusst.
Ethouris
Was ist der beste Weg, um Referenzen abzulehnen? Ich mache gerade static_cast<decltype(kConst)>(kConst).
Velkan
@Velkan Ich würde gerne wissen, wie das auch geht. Ihr Trick tatic_cast <decltype (kConst)> (kConst) funktioniert nicht, wenn kConst ein Zeichen ist [64]; es wird "Fehler: static_cast von 'char *' nach 'decltype (start_time)' (auch bekannt als 'char [64]') ist nicht erlaubt".
Don Hatch
@DonHatch, ich beschäftige mich nicht mit Software-Archäologie, aber soweit ich mich erinnere, ist es sehr schwierig, ein Raw-Array durch Kopieren in Funktion zu bringen. Syntaktisch benötigt die foo()ursprüngliche Frage die Adresse, und es gibt keinen Mechanismus, um sie als temporäre Kopie des gesamten Arrays zu behandeln.
Velkan
0

Ich hatte das gleiche Problem wie von Cloderic (statische Konstante in einem ternären Operator :) r = s ? kConst1 : kConst2, aber es hat sich erst nach dem Deaktivieren der Compileroptimierung beschwert (-O0 statt -Os). Passiert auf gcc-none-eabi 4.8.5.

Scg
quelle