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ć?


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


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;

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.


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

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

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

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.


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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.