Definiowanie stałej globalnej w C ++

81

Chcę zdefiniować stałą w C ++, aby była widoczna w kilku plikach źródłowych. Mogę sobie wyobrazić następujące sposoby zdefiniowania tego w pliku nagłówkowym:

  1. #define GLOBAL_CONST_VAR 0xFF
  2. int GLOBAL_CONST_VAR = 0xFF;
  3. Niektóre funkcje zachowujące wartość (np. int get_GLOBAL_CONST_VAR())
  4. enum { GLOBAL_CONST_VAR = 0xFF; }
  5. const int GLOBAL_CONST_VAR = 0xFF;
  6. extern const int GLOBAL_CONST_VAR; iw jednym pliku źródłowym const int GLOBAL_CONST_VAR = 0xFF;

Opcja (1) - zdecydowanie nie jest opcją, z której chciałbyś skorzystać

Opcja (2) - zdefiniowanie wystąpienia zmiennej w każdym pliku obiektowym za pomocą pliku nagłówkowego

Opcja (3) - IMO w większości przypadków zabija zbyt dużo

Opcja (4) - w wielu przypadkach może nie być dobra, ponieważ enum nie ma konkretnego typu (C ++ 0X doda możliwość zdefiniowania typu)

Dlatego w większości przypadków muszę wybrać między (5) a (6). Moje pytania:

  1. Co wolisz (5) czy (6)?
  2. Dlaczego (5) jest w porządku, a (2) nie?
dimba
źródło
1
5 kontra 2: „const” oznacza wewnętrzne powiązanie. Dołączając ten nagłówek wersji 5 do wielu jednostek tłumaczeniowych, nie naruszysz zasady „jednej definicji”. Ponadto const pozwala kompilatorowi na „zwijanie stałych”, podczas gdy wartość zmiennej innej niż stała może się zmieniać. Opcja 6 jest błędna. Potrzebujesz również "extern" w pliku cpp, aby wymusić zewnętrzne połączenie, w przeciwnym razie otrzymasz błędy konsolidatora. Opcja 6 ma tę zaletę, że ukrywa wartość. Ale także uniemożliwia ciągłe składanie.
sellibitze

Odpowiedzi:

32

(5) mówi dokładnie to, co chcesz powiedzieć. Dodatkowo pozwala kompilatorowi na optymalizację przez większość czasu. (6) z drugiej strony nie pozwoli kompilatorowi kiedykolwiek go zoptymalizować, ponieważ kompilator nie wie, czy w końcu go zmienisz, czy nie.

Blindy
źródło
1
OTOH, 5 jest technicznie niezgodne z prawem jako naruszenie ODR. Jednak większość kompilatorów zignoruje to.
Joel
Ech, wolę myśleć o tym jako o tym, że w ogóle niczego nie zdefiniowałem, po prostu powiedziałem kompilatorowi, aby nadał numerowi ładną nazwę. Tym właśnie jest (5), co oznacza brak narzutu w czasie wykonywania.
Blindy
2
Czy (5) stanowi naruszenie ODR? Jeśli tak, to preferowane jest (6). Dlaczego kompilator „nie wie, czy to zmienisz” w przypadku (6)? extern const int ...i const int ...czy oba są stałe, czyż nie?
D.Shawley,
4
AFAIK, między 5) a 6), tylko 6) jest dozwolone, gdy typ stałej nie jest oparty na int.
Klaim
11
Nie ma naruszenia ODR, obiekty stałe są domyślnie statyczne.
avakar
71

Zdecydowanie wybierz opcję 5 - jest bezpieczna dla typu i pozwala kompilatorowi na optymalizację (nie pobieraj adresu tej zmiennej :) Również jeśli jest w nagłówku - umieść ją w przestrzeni nazw, aby uniknąć zanieczyszczenia globalnego zasięgu:

// header.hpp
namespace constants
{
    const int GLOBAL_CONST_VAR = 0xFF;
    // ... other related constants

} // namespace constants

// source.cpp - use it
#include <header.hpp>
int value = constants::GLOBAL_CONST_VAR;
Nikolai Fetissov
źródło
3
Podczas próby dołączenia header.hppdo kilku plików źródłowych pojawia się błąd redefinicji .
LRDPRDX
Nie jestem pewien, dlaczego wciąż jest to głosowane - minęło już prawie dziesięć lat, ale teraz mamy constexpri wpisujemy wyliczenia na takie rzeczy.
Nikolai Fetissov
24

(5) jest „lepsze” niż (6), ponieważ definiuje się GLOBAL_CONST_VARjako Całkowe Wyrażenie Stałe (ICE) we wszystkich jednostkach translacyjnych. Na przykład, będziesz mógł używać go jako rozmiaru tablicy i jako etykiety przypadku we wszystkich jednostkach tłumaczeniowych. W przypadku (6) GLOBAL_CONST_VARbędzie ICE tylko w tej jednostce tłumaczeniowej, w której jest zdefiniowany i tylko po punkcie definicji. W innych jednostkach tłumaczeniowych nie będzie działać jako ICE.

Należy jednak pamiętać, że (5) zapewnia GLOBAL_CONST_VARpowiązanie wewnętrzne, co oznacza, że ​​„tożsamość adresu” GLOBAL_CONST_VARbędzie inna w każdej jednostce tłumaczeniowej, tj.&GLOBAL_CONST_VAR Poda inną wartość wskaźnika w każdej jednostce tłumaczeniowej. W większości przypadków nie ma to znaczenia, ale jeśli potrzebujesz stałego obiektu, który ma spójną globalną „tożsamość adresową”, musiałbyś iść z (6), poświęcając ICE-ność stałej w proces.

Również, gdy ICE-ność stałej nie jest problemem (nie jest to typ całkowy), a rozmiar typu rośnie (nie jest to typ skalarny), wtedy (6) zwykle staje się lepszym podejściem niż (5).

(2) nie jest OK, ponieważ GLOBAL_CONST_VARin (2) ma domyślnie łącze zewnętrzne. Jeśli umieścisz go w pliku nagłówkowym, zwykle otrzymasz wiele definicji GLOBAL_CONST_VAR, co jest błędem. constobiekty w C ++ mają domyślnie wewnętrzne powiązanie, dlatego (5) działa (i dlatego, jak powiedziałem powyżej, otrzymujesz oddzielną, niezależną GLOBAL_CONST_VARw każdej jednostce tłumaczeniowej).


Począwszy od C ++ 17 masz możliwość zadeklarowania

inline extern const int GLOBAL_CONST_VAR = 0xFF;

w pliku nagłówkowym. Daje to ICE we wszystkich jednostkach tłumaczeniowych (podobnie jak metoda (5)), jednocześnie zachowując globalną tożsamość adresową GLOBAL_CONST_VAR- we wszystkich jednostkach tłumaczeniowych będzie miał ten sam adres.

Mrówka
źródło
8

Jeśli używasz C ++ 11 lub nowszego, spróbuj użyć stałych czasu kompilacji:

constexpr int GLOBAL_CONST_VAR{ 0xff };
xninja
źródło
1
IMHO, to jedyne satysfakcjonujące rozwiązanie tego problemu.
lanoxx
5

Jeśli ma to być stała, to oznacz ją jako stałą - dlatego moim zdaniem 2 jest złe.

Kompilator może użyć stałej natury wartości, aby rozwinąć niektóre matematyki, a nawet inne operacje, które używają tej wartości.

Wybór między 5 a 6 - hmm; Po prostu czuję się lepiej dla mnie.

W 6) wartość jest niepotrzebnie oddzielana od deklaracji.

Zwykle miałbym jeden lub więcej takich nagłówków, które definiują w nich tylko stałe itp., A następnie żadnych innych „sprytnych” rzeczy - ładne, lekkie nagłówki, które można łatwo umieścić w dowolnym miejscu.

Andras Zoltan
źródło
3
(6) nie jest niepotrzebnym dystansem, jest to celowy wybór. Jeśli masz dużo dużych, stałych, marnujesz dużo miejsca w pliku wykonywalnym, jeśli nie zadeklarujesz ich jak w (6). Może się to zdarzyć w bibliotekach matematycznych ... marnotrawstwo może wynosić poniżej 100 000, ale czasami nawet to jest ważne. (Niektóre kompilatory mają inne sposoby obejścia tego problemu, myślę, że MSVC ma atrybut „Once” lub coś podobnego.)
Dan Olson,
Z (5) nie możesz być naprawdę pewien, że pozostanie const (zawsze możesz odrzucić stałość). Dlatego nadal wolałbym typ wyliczeniowy.
fmuecke
@Dan Olson - to bardzo dobra uwaga - Moja odpowiedź opierała się na fakcie, że typem tutaj jest int; ale w przypadku większych wartości deklaracja zewnętrzna jest rzeczywiście lepszym planem.
Andras Zoltan
@fmuecke - Tak, masz rację - w takim przypadku wartość wyliczenia temu zapobiega. Ale czy to oznacza, że ​​powinniśmy zawsze chronić nasze wartości przed pisaniem w ten sposób? Jeśli programista chce nadużywać kodu, jest tak wiele obszarów, w których rzutowanie (target_type *) ((void *) & value) może siać spustoszenie, że nie możemy ich złapać, że czasami musimy po prostu im zaufać; i rzeczywiście siebie, nie?
Andras Zoltan
@fmuecke Zmienna, która jest zadeklarowana jako stała, nie może zostać zmieniona przez program (próba zrobienia tego jest niezdefiniowanym zachowaniem). const_cast jest definiowane tylko w sytuacjach, w których oryginalna zmienna nie została zadeklarowana jako stała (na przykład, przekazując wartość inną niż stała do funkcji jako stała &).
David Stone
5

Aby odpowiedzieć na drugie pytanie:

(2) jest niezgodne z prawem, ponieważ narusza zasadę jednej definicji. Określa GLOBAL_CONST_VARw każdym pliku, w którym jest zawarty, tj. Więcej niż raz. (5) jest legalny, ponieważ nie podlega zasadzie jednej definicji. Każda GLOBAL_CONST_VARjest oddzielną definicją lokalną dla tego pliku, w którym jest zawarta. Wszystkie te definicje mają oczywiście tę samą nazwę i wartość, ale ich adresy mogą się różnić.

MSalters
źródło
4

inlineZmienne C ++ 17

Ta niesamowita funkcja C ++ 17 pozwala nam:

  • wygodnie używać tylko jednego adresu pamięci dla każdej stałej
  • zapisz to jako constexpr: Jak zadeklarować extern constexpr?
  • zrób to w jednej linii z jednego nagłówka

main.cpp

#include <cassert>

#include "notmain.hpp"

int main() {
    // Both files see the same memory address.
    assert(&notmain_i == notmain_func());
    assert(notmain_i == 42);
}

notmain.hpp

#ifndef NOTMAIN_HPP
#define NOTMAIN_HPP

inline constexpr int notmain_i = 42;

const int* notmain_func();

#endif

notmain.cpp

#include "notmain.hpp"

const int* notmain_func() {
    return &notmain_i;
}

Skompiluj i uruchom:

g++ -c -o notmain.o -std=c++17 -Wall -Wextra -pedantic notmain.cpp
g++ -c -o main.o -std=c++17 -Wall -Wextra -pedantic main.cpp
g++ -o main -std=c++17 -Wall -Wextra -pedantic main.o notmain.o
./main

GitHub upstream .

Zobacz także: Jak działają zmienne wbudowane?

Standard C ++ dotyczący zmiennych wbudowanych

Standard C ++ gwarantuje, że adresy będą takie same. C ++ 17 N4659 standardowa wersja robocza 10.1.6 „Specyfikator wbudowany”:

6 Funkcja lub zmienna inline z połączeniem zewnętrznym będzie mieć ten sam adres we wszystkich jednostkach tłumaczeniowych.

cppreference https://en.cppreference.com/w/cpp/language/inline wyjaśnia, że ​​jeśli staticnie jest podane, ma link zewnętrzny.

Implementacja zmiennych inline

Możemy obserwować, jak jest realizowany za pomocą:

nm main.o notmain.o

który zawiera:

main.o:
                 U _GLOBAL_OFFSET_TABLE_
                 U _Z12notmain_funcv
0000000000000028 r _ZZ4mainE19__PRETTY_FUNCTION__
                 U __assert_fail
0000000000000000 T main
0000000000000000 u notmain_i

notmain.o:
0000000000000000 T _Z12notmain_funcv
0000000000000000 u notmain_i

i man nmmówi o u:

„u” Symbol jest unikalnym symbolem globalnym. To jest rozszerzenie GNU do standardowego zestawu powiązań symboli ELF. Dla takiego symbolu dynamiczny linker upewni się, że w całym procesie jest tylko jeden symbol o tej nazwie i typie w użyciu.

więc widzimy, że jest do tego dedykowane rozszerzenie ELF.

Testowane na GCC 7.4.0, Ubuntu 18.04.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
źródło
2
const int GLOBAL_CONST_VAR = 0xFF;

ponieważ jest stała!


źródło
1
I nie będzie traktowane jak makro, co ułatwi debugowanie.
kayleeFrye_onDeck
-1, spowoduje to redefinicję ostrzeżeń / błędów przy włączaniu nagłówka do wielu plików źródłowych. Również ta odpowiedź jest duplikatem odpowiedzi Nikołaja Fetissowa.
lanoxx,
1

To zależy od twoich wymagań. (5) jest najlepszy do większości normalnych zastosowań, ale często powoduje ciągłe zajmowanie miejsca w każdym pliku obiektowym. (6) mogą obejść ten problem w sytuacjach, w których jest to ważne.

(4) jest również dobrym wyborem, jeśli Twoim priorytetem jest zagwarantowanie, że przestrzeń dyskowa nigdy nie zostanie przydzielona, ​​ale działa tylko dla stałych całkowitych.

Dan Olson
źródło
1
#define GLOBAL_CONST_VAR 0xFF // this is C code not C++
int GLOBAL_CONST_VAR = 0xFF; // it is not constant and maybe not compilled
Some function returing the value (e.g. int get_LOBAL_CONST_VAR()) // maybe but exists better desision
enum { LOBAL_CONST_VAR = 0xFF; } // not needed, endeed, for only one constant (enum elms is a simple int, but with secial enumeration)
const int GLOBAL_CONST_VAR = 0xFF; // it is the best
extern const int GLOBAL_CONST_VAR; //some compiller doesn't understand this
den bardadym
źródło