Niezdefiniowane odniesienie do stałej statycznej int

79

Dzisiaj natknąłem się na ciekawy problem. Rozważmy ten prosty przykład:

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();
}

Podczas kompilacji pojawia się błąd:

Undefined reference to 'Bar::kConst'

Teraz jestem prawie pewien, że dzieje się tak dlatego, że static const intnigdzie nie jest zdefiniowany, co jest zamierzone, ponieważ zgodnie z moim zrozumieniem kompilator powinien być w stanie dokonać zamiany w czasie kompilacji i nie potrzebować definicji. Jednak ponieważ funkcja przyjmuje const int &parametr, wydaje się, że nie dokonuje podstawienia, a zamiast tego preferuje odniesienie. Mogę rozwiązać ten problem, wprowadzając następującą zmianę:

foo(static_cast<int>(kConst));

Wydaje mi się, że teraz zmusza to kompilator do utworzenia tymczasowego int, a następnie przekazania do niego odniesienia, co może z powodzeniem wykonać w czasie kompilacji.

Zastanawiałem się, czy było to zamierzone, czy też oczekuję zbyt wiele od gcc, aby poradzić sobie z tym przypadkiem? A może jest to coś, czego z jakiegoś powodu nie powinienem robić?

JaredC
źródło
1
W praktyce możesz po prostu osiągnąć const int kConst = 1;ten sam wynik. Poza tym rzadko istnieje powód (nie przychodzi mi do głowy żaden), aby funkcja przyjmowała parametr typu const int &- po prostu użyj inttutaj.
Björn Pollex
1
@Space prawdziwą funkcją był szablon, edytuję moje pytanie, aby o tym wspomnieć.
JaredC,
1
@Space fyi, nie tworząc go, staticpowoduje błąd `` ISO C ++ zabrania inicjalizacji elementu 'kConst' ... czyniąc 'kConst' statycznym. '
JaredC,
Mój błąd, dzięki za korektę.
Björn Pollex
1
Irytujące jest to, błąd ten może pokazać w nieszkodliwych zastosowań takich jak std::min( some_val, kConst), gdyż std::min<T>posiada parametry typu T const &, a implikacja jest taka, że musimy przekazać referencję do tej kConst. Okazało się, że wystąpiło to tylko wtedy, gdy optymalizacja była wyłączona. Naprawiono przy użyciu statycznej obsady.
greggo

Odpowiedzi:

61

To celowe, 9.4.2 / 4 mówi:

Jeśli statyczny element członkowski danych jest typu const integra lub const wyliczenia, jego deklaracja w definicji klasy może określać inicjalizator stałej, który powinien być integralnym wyrażeniem stałym (5.19). W takim przypadku element członkowski może pojawić się w wyrażeniach stałych całkowitych. Element członkowski powinien nadal być zdefiniowany w zakresie przestrzeni nazw, jeśli jest używany w programie

Przekazując statyczny element członkowski danych przez odwołanie do stałej, „używasz” go, 3.2 / 2:

Wyrażenie jest potencjalnie oceniane, chyba że pojawia się tam, gdzie wymagane jest całkowe wyrażenie stałe (patrz 5.19), jest operandem operatora sizeof (5.3.3) lub operandem operatora typeid, a wyrażenie nie wyznacza lwartości typ klasy polimorficznej (5.2.8). Obiekt lub nie przeciążona funkcja jest używana, jeśli jej nazwa pojawia się w potencjalnie ocenianym wyrażeniu.

Tak więc w rzeczywistości „używasz” go, gdy przekazujesz go również przez wartość lub w pliku static_cast. Po prostu GCC uwolniło cię od haczyka w jednym przypadku, ale nie w drugim.

[Edycja: gcc stosuje reguły z wersji roboczych C ++ 0x: "Zmienna lub nie przeciążona funkcja, której nazwa pojawia się jako potencjalnie oceniane wyrażenie, jest używana na zasadzie odr, chyba że jest to obiekt, który spełnia wymagania dotyczące pojawienia się w stałej wyrażenie (5.19) i konwersja lwartości do rwartości (4.1) jest natychmiast stosowana. ”. Rzutowanie statyczne natychmiast wykonuje konwersję lvalue-rvalue, więc w C ++ 0x nie jest „używane”.]

Praktyczny problem z odwołaniem do const polega na tym, że fooma on prawo wziąć adres swojego argumentu i porównać go na przykład z adresem argumentu z innego wywołania, przechowywanego w zmiennej globalnej. Ponieważ statyczny element członkowski danych jest unikalnym obiektem, oznacza to, że jeśli wywołujesz foo(kConst)z dwóch różnych jednostek tłumaczeniowych, adres przekazanego obiektu musi być w każdym przypadku taki sam. AFAIK GCC nie może tego zorganizować, chyba że obiekt jest zdefiniowany w jednej (i tylko jednej) jednostce tłumaczeniowej.

OK, więc w tym przypadku foojest to szablon, stąd definicja jest widoczna we wszystkich jednostkach tłumaczeniowych, więc być może kompilator teoretycznie mógłby wykluczyć ryzyko, że zrobi cokolwiek z adresem. Ale generalnie na pewno nie powinieneś brać adresów lub odwołań do nieistniejących obiektów ;-)

Steve Jessop
źródło
1
Dzięki za przykład zabrania adresu referencyjnego. Myślę, że to prawdziwy praktyczny powód, dla którego kompilator nie robi tego, czego oczekuję.
JaredC,
Myślę, że dla pełnej zgodności musiałbyś zdefiniować coś takiego template <int N> int intvalue() { return N; }. Wtedy z intvalue<kConst>, kConstpojawia się tylko w kontekście wymagającym integralnego stałego wyrażenia, więc nie jest używane. Ale funkcja zwraca obiekt tymczasowy o tej samej wartości co kConst, i może wiązać się z odwołaniem do stałej. Nie jestem jednak pewien, czy może istnieć prostszy sposób przenoszenia, który kConstnie jest używany.
Steve Jessop,
1
Doświadczam tego samego problemu, używając takiej statycznej zmiennej const w operatorze trójskładnikowym (tj. Coś podobnego r = s ? kConst1 : kConst2) z gcc 4.7. Rozwiązałem to, używając rzeczywistego pliku if. W każdym razie dzięki za odpowiedź!
Clodéric
2
... i std :: min / std :: max, które doprowadziły mnie tutaj!
mędrzec
„AFAIK GCC nie może tego zorganizować, chyba że obiekt jest zdefiniowany w jednej (i tylko jednej) jednostce TU”. Szkoda, ponieważ można to już zrobić ze stałymi - skompiluj je wiele razy jako „słabe definicje” w .rodata, a następnie pozwól, aby linker wybrał tylko jeden - co zapewnia, że ​​wszystkie rzeczywiste odwołania do niego będą miały ten sam adres. To jest właśnie to, co robi się dla typeid; może jednak zawieść w dziwny sposób, gdy używane są biblioteki współdzielone.
greggo
27

Jeśli piszesz statyczną zmienną const z inicjatorem wewnątrz deklaracji klasy, to tak, jakbyś pisał

class Bar
{
      enum { kConst = 1 };
}

a GCC potraktuje go w ten sam sposób, co oznacza, że ​​nie ma adresu.

Powinien to być poprawny kod

class Bar
{
      static const int kConst;
}
const int Bar::kConst = 1;
pelya
źródło
Dziękuję za ten ilustracyjny przykład.
shuhalo
12

To naprawdę ważny przypadek. Zwłaszcza, że foo może być funkcją z STL, taką jak std :: count, która przyjmuje stałą T & jako trzeci argument.

Spędziłem dużo czasu próbując zrozumieć, dlaczego konsolidator miał problemy z tak podstawowym kodem.

Komunikat o błędzie

Niezdefiniowane odniesienie do „Bar :: kConst”

mówi nam, że linker nie może znaleźć symbolu.

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

Widzimy z 'U', że Bar :: kConst jest niezdefiniowany. Stąd, kiedy linker próbuje wykonać swoją pracę, musi znaleźć symbol. Ale deklarujesz tylko kConst i nie definiujesz go.

Rozwiązaniem w C ++ jest również zdefiniowanie tego w następujący sposób:

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();
}

Następnie widać, że kompilator umieści definicję w wygenerowanym pliku obiektowym:

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

Teraz możesz zobaczyć „R” mówiącą, że jest zdefiniowana w sekcji danych.

Stac
źródło
Czy to w porządku, że stała pojawia się dwukrotnie na wyjściu „nm-C”, najpierw z „R” i adresem, a następnie z „U”?
quant_dev
Masz jakiś przykład? W podanym przykładzie >nm -C main.o | grep kConstdaje mi tylko jedną linię 0000000000400644 R Bar::kConst.
Stac
Widzę to podczas kompilacji biblioteki statycznej.
quant_dev
1
W tym przypadku oczywiście! Biblioteka statyczna jest tylko agregatem plików obiektowych. Powiązanie jest wykonywane tylko przez klienta biblioteki statycznej. Więc jeśli umieścisz w pliku archiwum plik obiektowy zawierający definicję stałej i inny plik obiektowy wywołujący Bar :: func (), zobaczysz symbol raz z definicją, a raz bez: nm -C lib.adaje Constants.o: 0000000000000000 R Bar::kConsti main_file.o: U Bar::kConst ....
Stac
2

g ++ wersja 4.3.4 akceptuje ten kod (zobacz ten link ). Ale g ++ wersja 4.4.0 odrzuca to.

TonyK
źródło
2

Myślę, że ten artefakt C ++ oznacza, że ​​za każdym razem, gdy Bar::kConstjest mowa, używana jest jego wartość dosłowna.

Oznacza to, że w praktyce nie ma zmiennej, do której można by się odwołać.

Być może będziesz musiał to zrobić:

void func()
{
  int k = kConst;
  foo(k);
}
quamrana
źródło
Zasadniczo to właśnie osiągnąłem, zmieniając to na foo(static_cast<int>(kConst));, prawda?
JaredC,
2

Możesz również zastąpić go funkcją składową constexpr:

class Bar
{
  static constexpr int kConst() { return 1; };
};
Yoav
źródło
Zauważ, że wymaga to zarówno 4 wierszy w deklaracji, jak i nawiasów klamrowych po "stałej", więc kończysz pisząc foo = std :: numeric_limits <int> :: max () * bar :: this_is_a_constant_that_looks_like_a_method () i masz nadzieję, że twój standard kodowania sobie poradzi a optymalizator naprawi to za Ciebie.
Code Abominator
1

Prosta sztuczka: użyj +przed kConstprzekazaniem funkcji. Zapobiegnie to pobieraniu odwołania do stałej, a w ten sposób kod nie wygeneruje żądania konsolidatora do obiektu stałego, ale zamiast tego będzie kontynuował z wartością stałą czasu kompilatora.

Ethouris
źródło
Szkoda jednak, że kompilator nie wyświetla ostrzeżenia, gdy adres jest pobierany z static constwartości inicjowanej w deklaracji. Zawsze prowadziłoby to do błędu konsolidatora, a gdy ta sama stała jest również deklarowana oddzielnie w pliku obiektowym, byłby to również błąd. Kompilator jest również w pełni świadomy sytuacji.
Ethouris
Jaki jest najlepszy sposób na odrzucenie referencji? Obecnie robię static_cast<decltype(kConst)>(kConst).
Velkan
@Velkan Też chciałbym wiedzieć, jak to zrobić. Twoja sztuczka tatic_cast <decltype (kConst)> (kConst) nie działa w przypadku, gdy kConst jest char [64]; dostaje „błąd: static_cast z„ char * ”do„ decltype (start_time) ”(aka„ char [64] ”) jest niedozwolone”.
Don Hatch
@DonHatch, nie interesuję się archeologią oprogramowania, ale o ile pamiętam, bardzo trudno jest przekazać surową tablicę do funkcji przez kopiowanie. Tak więc składniowo foo()pytanie z pierwotnego pytania będzie wymagało adresu i nie ma mechanizmu, aby traktować go jako tymczasową kopię całej tablicy.
Velkan
0

Doświadczyłem tego samego problemu, o którym wspomniał Cloderic (statyczna stała w operatorze trójskładnikowym:) r = s ? kConst1 : kConst2, ale narzekał tylko po wyłączeniu optymalizacji kompilatora (-O0 zamiast -Os). Stało się to na gcc-none-eabi 4.8.5.

Scg
źródło