>7.10 — Współdzielenie stałych globalnych w wielu plikach (przy użyciu zmiennych wbudowanych)

W niektórych aplikacjach może być konieczne użycie pewnych stałych symbolicznych w całym kodzie (nie tylko w jednym miejscu). Mogą to być stałe fizyczne lub matematyczne, które się nie zmieniają (np. pi lub liczba Avogadro) lub wartości „dostrojenia” specyficzne dla aplikacji (np. współczynniki tarcia lub grawitacji). Zamiast przedefiniowywać te stałe w każdym pliku, który ich potrzebuje (naruszenie zasady „Nie powtarzaj się”), lepiej zadeklarować je raz w centralnej lokalizacji i używać ich tam, gdzie zajdzie taka potrzeba. Dzięki temu, jeśli kiedykolwiek będziesz musiał je zmienić, wystarczy, że zmienisz je w jednym miejscu, a zmiany te będą mogły zostać rozpowszechnione.

W tej lekcji omówiono najczęstsze sposoby, aby to zrobić.

Stałe globalne jako zmienne wewnętrzne

Przed C++17 najłatwiejszym i najczęstszym rozwiązaniem jest następujące rozwiązanie:

  1. Utwórz plik nagłówkowy do przechowywania tych stałe
  2. Wewnątrz tego pliku nagłówkowego zdefiniuj przestrzeń nazw (omówiono to w lekcji 7.2 -- Przestrzenie nazw zdefiniowane przez użytkownika i operator rozpoznawania zakresu)
  3. Dodaj wszystkie stałe w przestrzeni nazw (upewnij się, że są constexpr)
  4. #include plik nagłówkowy tam, gdzie go potrzebujesz

Na przykład:

stałe.h:

#ifndef CONSTANTS_H
#define CONSTANTS_H

// Define your own namespace to hold constants
namespace constants
{
    // Global constants have internal linkage by default
    constexpr double pi { 3.14159 };
    constexpr double avogadro { 6.0221413e23 };
    constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
    // ... other related constants
}
#endif

Następnie użyj operatora rozpoznawania zakresu (::) z nazwą przestrzeni nazw po lewej stronie i nazwą zmiennej po prawej, aby uzyskać dostęp do stałych pliki .cpp:

main.cpp:

#include "constants.h" // include a copy of each constant in this file

#include <iostream>

int main()
{
    std::cout << "Enter a radius: ";
    double radius{};
    std::cin >> radius;

    std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';

    return 0;
}

Kiedy ten nagłówek zostanie #włączony do pliku .cpp, każda ze zmiennych zdefiniowanych w nagłówku zostanie skopiowana do tego pliku z kodem w momencie włączenia. Ponieważ zmienne te znajdują się poza funkcją, są traktowane jako zmienne globalne w pliku, w którym są zawarte, dlatego można ich używać w dowolnym miejscu tego pliku.

Ponieważ globale const mają wewnętrzne powiązanie, każda z nich jest zmienna globalna. Plik .cpp pobiera niezależną wersję zmiennej globalnej, której linker nie widzi. W większości przypadków, ponieważ są to constexpr, kompilator po prostu zoptymalizuje zmienne.

Chociaż jest to proste (i dobre dla mniejszych programów), za każdym razem, gdy constants.h jest #włączany do innego pliku z kodem, każda z tych zmiennych jest kopiowana do pliku z kodem zawierającym. jeśli plik const.h zostanie włączony do 20 różnych plików z kodem, każda z tych zmiennych zostanie zduplikowana 20 razy. Osłony nagłówka nie zapobiegną temu zjawisku, ponieważ zapobiegają jedynie wielokrotnemu włączeniu nagłówka do jednego pliku zawierającego, a nie jednorazowemu włączeniu do wielu różnych plików z kodem. Wprowadza to dwa wyzwania:

  1. Zmiana pojedynczej wartości stałej wymagałaby ponownej kompilacji każdego pliku zawierającego nagłówek stałych, co może prowadzić do długich czasów przebudowy. większe projekty.
  2. Jeśli stałe mają duży rozmiar i nie można ich zoptymalizować, może to zająć dużo pamięci.

Zalety:

  • Działa przed C++ 17.
  • Można używać w wyrażeniach stałych w dowolnej jednostce tłumaczenia, która je zawiera.

Wady:

  • Zmiana czegokolwiek w pliku nagłówkowym wymaga ponownej kompilacji plików łącznie z nagłówkiem.
  • Każda jednostka tłumacząca, w tym nagłówek, otrzymuje własną kopię zmiennej.

Stałe globalne jako zmienne zewnętrzne

Jeśli aktywnie zmieniasz wartości lub dodajesz nowe stałe, wcześniejsze rozwiązanie może być problematyczne, przynajmniej do czasu, aż wszystko się uspokoi.

Jednym ze sposobów uniknięcia tych problemów jest zmiana tych stałych na zmienne zewnętrzne, ponieważ możemy wówczas mieć pojedynczą zmienną (jednorazowo zainicjalizowaną), która jest współdzielona we wszystkich plikach. W tej metodzie zdefiniujemy stałe w pliku .cpp (aby mieć pewność, że definicje istnieją tylko w jednym miejscu) i umieścimy deklaracje w nagłówku (które zostaną uwzględnione w innych plikach).

constants.cpp:

#include "constants.h"

namespace constants
{
    // We use extern to ensure these have external linkage
    extern constexpr double pi { 3.14159 };
    extern constexpr double avogadro { 6.0221413e23 };
    extern constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
}

stałe.h:

#ifndef CONSTANTS_H
#define CONSTANTS_H

namespace constants
{
    // Since the actual variables are inside a namespace, the forward declarations need to be inside a namespace as well
    // We can't forward declare variables as constexpr, but we can forward declare them as (runtime) const
    extern const double pi;
    extern const double avogadro;
    extern const double myGravity;
}

#endif

Zastosowanie w pliku kodu pozostaje takie samo:

main.cpp:

#include "constants.h" // include all the forward declarations

#include <iostream>

int main()
{
    std::cout << "Enter a radius: ";
    double radius{};
    std::cin >> radius;

    std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';

    return 0;
}

Teraz instancja stałych symbolicznych zostanie utworzona tylko raz (w constants.cpp) zamiast w każdym pliku kodu, w którym constants.h jest #uwzględniony, a wszystkie użycia tych stałych będą powiązane z utworzoną wersją w constants.cpp. Wszelkie zmiany wprowadzone w constants.cpp będą wymagać jedynie ponownej kompilacji constants.cpp.

Jednak istnieje kilka wad tej metody. Po pierwsze, ponieważ constexpr są tylko definicjami zmiennych (deklaracje forward nie są i nie mogą być), te stałe są wyrażeniami stałymi tylko w pliku, w którym są faktycznie zdefiniowane (constants.cpp). W innych plikach kompilator zobaczy tylko deklarację forward, która nie definiuje wartości constexpr (i musi zostać rozwiązana przez linker). Oznacza to, że poza plikiem, w którym są zdefiniowane, zmienne te nie mogą być używane w wyrażeniach stałych. Po drugie, ponieważ wyrażenia stałe można zwykle optymalizować w większym stopniu niż wyrażenia środowiska wykonawczego, kompilator może nie być w stanie zoptymalizować ich w tak dużym stopniu.

Kluczowa informacja

Aby zmienne nadawały się do użycia w kontekstach kompilacji, takich jak rozmiary tablic, kompilator musi zobaczyć definicję zmiennej (a nie tylko deklarację forward).

Ponieważ kompilator kompiluje każdy plik źródłowy indywidualnie, widzi tylko definicje zmiennych, które pojawiają się w kompilowanym pliku źródłowym (który zawiera wszystkie dołączone nagłówki). Na przykład definicje zmiennych w constants.cpp nie są widoczne, gdy kompilator kompiluje main.cpp. Z tego powodu zmiennych constexpr nie można rozdzielić na plik nagłówkowy i plik źródłowy, należy je zdefiniować w pliku nagłówkowym.

Biorąc pod uwagę powyższe wady, preferuj definiowanie stałych w pliku nagłówkowym (albo według poprzedniej sekcji, albo według następnej sekcji). Jeśli okaże się, że wartości stałych często się zmieniają (np. ponieważ dostrajasz program) i prowadzi to do długiego czasu kompilacji, możesz tymczasowo przenieść tylko te stałe, które powodują błędy do pliku .cpp (używając tej metody).

Zalety:

  • Działa przed C++ 17.
  • Wymagana jest tylko jedna kopia każdej zmiennej.
  • Wymaga rekompilacji jednego pliku tylko w przypadku zmiany wartości stałej.

Wady:

  • Deklaracja przekazywania i definicje zmiennych znajdują się w oddzielnych plikach i muszą być synchronizowane.
  • Zmiennych nie można używać w wyrażeniach stałych poza plikiem, w którym są zdefiniowane.

Stałe globalne jako zmienne inline C++17

W lekcji 7.9 -- Funkcje wbudowane i zmienne, omówiliśmy zmienne inline, czyli zmienne, które mogą mieć więcej niż jedną definicję, pod warunkiem, że te definicje są identyczne. Umieszczając nasze zmienne constexpr w tekście, możemy zdefiniować je w pliku nagłówkowym, a następnie #include je do dowolnego pliku .cpp, który ich wymaga. Pozwala to uniknąć zarówno naruszeń ODR, jak i negatywnych skutków duplikacji zmiennych.

Przypomnienie

Funkcje Constexpr są domyślnie wbudowane, ale zmienne constexpr nie są domyślnie wbudowane. Jeśli chcesz mieć wbudowaną zmienną constexpr, musisz jawnie oznaczyć ją jako inline.

Kluczowa informacja

Zmienne wbudowane mają domyślnie zewnętrzne powiązanie, dzięki czemu są widoczne dla linkera. Jest to konieczne, aby linker mógł usunąć duplikaty definicji.

Nieinline zmienne constexpr mają wewnętrzne powiązania. Jeśli zostanie uwzględniona w wielu jednostkach tłumaczeniowych, każda jednostka tłumaczeniowa otrzyma własną kopię zmiennej. Nie stanowi to naruszenia ODR, ponieważ nie są one widoczne dla linkera.

stałe.h:

#ifndef CONSTANTS_H
#define CONSTANTS_H

// define your own namespace to hold constants
namespace constants
{
    inline constexpr double pi { 3.14159 }; // note: now inline constexpr
    inline constexpr double avogadro { 6.0221413e23 };
    inline constexpr double myGravity { 9.2 }; // m/s^2 -- gravity is light on this planet
    // ... other related constants
}
#endif

main.cpp:

#include "constants.h"

#include <iostream>

int main()
{
    std::cout << "Enter a radius: ";
    double radius{};
    std::cin >> radius;

    std::cout << "The circumference is: " << 2 * radius * constants::pi << '\n';

    return 0;
}

Możemy włączyć constants.h do dowolnej liczby plików kodu, ale instancja tych zmiennych zostanie utworzona tylko raz i udostępniona we wszystkich plikach kodu.

Ta metoda ma tę wadę, że wymaga ponownej kompilacji każdego pliku zawierającego nagłówek stałych w przypadku zmiany dowolnej wartości stałej.

Zalety:

  • Można używać w wyrażeniach stałych w dowolnej jednostce tłumaczenia, która je zawiera.
  • Wymagana jest tylko jedna kopia każdej zmiennej.

Wady:

  • Działa tylko w C++17 i nowsze.
  • Zmiana czegokolwiek w pliku nagłówkowym wymaga ponownej kompilacji plików łącznie z nagłówkiem.

Najlepsza praktyka

Jeśli potrzebujesz stałych globalnych, a twój kompilator obsługuje C++17, preferuj definiowanie wbudowanych zmiennych globalnych constexpr w pliku nagłówkowym.

Przypomnienie

Użyj std::string_view for constexpr ciągi znaków. Omówimy to na lekcji 5.8 — Wprowadzenie do std::string_view.

Powiązana treść

Podsumowujemy zakres, czas trwania i powiązania różnych rodzajów zmiennych na lekcji 7.12 — Podsumowanie zakresu, czasu trwania i powiązania.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
397 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze