Do czego służą wbudowane przestrzenie nazw?

334

C ++ 11 zezwala na inline namespaces, których wszyscy członkowie są również automatycznie dołączani namespace. Nie mogę wymyślić żadnego przydatnego zastosowania tego - czy ktoś mógłby podać krótki, zwięzły przykład sytuacji, w której jest inline namespaceto potrzebne i gdzie jest to najbardziej idiomatyczne rozwiązanie?

(Poza tym nie jest dla mnie jasne, co się stanie, gdy namespacedeklaracja zostanie zadeklarowana inlinew jednym, ale nie we wszystkich deklaracjach, które mogą znajdować się w różnych plikach. Czy to nie błaga o kłopoty?)

Walter
źródło

Odpowiedzi:

339

Wewnętrzne przestrzenie nazw są funkcją wersjonowania biblioteki podobną do wersji symboli , ale zaimplementowaną wyłącznie na poziomie C ++ 11 (tj. Międzyplatformowym) zamiast być cechą określonego binarnego formatu wykonywalnego (tj. Specyficznego dla platformy).

Jest to mechanizm, dzięki któremu autor biblioteki może sprawić, aby zagnieżdżona przestrzeń nazw wyglądała i działała tak, jakby wszystkie jej deklaracje znajdowały się w otaczającej przestrzeni nazw (wbudowane przestrzenie nazw można zagnieżdżać, dzięki czemu nazwy „bardziej zagnieżdżone” przenikają aż do pierwszego -inline przestrzeń nazw i wygląda i działa tak, jakby ich deklaracje znajdowały się również w jednej z przestrzeni nazw pomiędzy nimi).

Jako przykład rozważ implementację STL vector. Gdybyśmy mieli wbudowane przestrzenie nazw od początku C ++, to w C ++ 98 nagłówek <vector>mógłby wyglądać tak:

namespace std {

#if __cplusplus < 1997L // pre-standard C++
    inline
#endif

    namespace pre_cxx_1997 {
        template <class T> __vector_impl; // implementation class
        template <class T> // e.g. w/o allocator argument
        class vector : __vector_impl<T> { // private inheritance
            // ...
        };
    }
#if __cplusplus >= 1997L // C++98/03 or later
                         // (ifdef'ed out b/c it probably uses new language
                         // features that a pre-C++98 compiler would choke on)
#  if __cplusplus == 1997L // C++98/03
    inline
#  endif

    namespace cxx_1997 {

        // std::vector now has an allocator argument
        template <class T, class Alloc=std::allocator<T> >
        class vector : pre_cxx_1997::__vector_impl<T> { // the old impl is still good
            // ...
        };

        // and vector<bool> is special:
        template <class Alloc=std::allocator<bool> >
        class vector<bool> {
            // ...
        };

    };

#endif // C++98/03 or later

} // namespace std

W zależności od wartości wybierana jest __cplusplusjedna lub druga vectorimplementacja. Jeśli codebase został napisany w pre-C ++ 98 razy, a okaże się, że C ++ 98 wersja vectorjest przyczyną kłopotów dla ciebie, kiedy uaktualnić kompilator, „wszystkie”, co musisz zrobić, to znaleźć odniesienia do std::vectorw twoją bazę kodów i zastąp je std::pre_cxx_1997::vector.

Przyjdź następny standard, a sprzedawca STL po prostu ponownie powtarza procedurę, wprowadzając nową przestrzeń nazw std::vectorz emplace_backobsługą (która wymaga C ++ 11) i wstawiając tę ​​iff __cplusplus == 201103L.

OK, więc dlaczego potrzebuję do tego nowej funkcji językowej? Mogę już wykonać następujące czynności, aby uzyskać ten sam efekt, nie?

namespace std {

    namespace pre_cxx_1997 {
        // ...
    }
#if __cplusplus < 1997L // pre-standard C++
    using namespace pre_cxx_1997;
#endif

#if __cplusplus >= 1997L // C++98/03 or later
                         // (ifdef'ed out b/c it probably uses new language
                         // features that a pre-C++98 compiler would choke on)

    namespace cxx_1997 {
        // ...
    };
#  if __cplusplus == 1997L // C++98/03
    using namespace cxx_1997;
#  endif

#endif // C++98/03 or later

} // namespace std

W zależności od wartości __cplusplusotrzymuję jedną z tych implementacji.

I miałbyś prawie rację.

Rozważ następujący poprawny kod użytkownika C ++ 98 (dozwolone było w pełni specjalizowanie szablonów, które już istnieją w przestrzeni nazw stdw C ++ 98):

// I don't trust my STL vendor to do this optimisation, so force these 
// specializations myself:
namespace std {
    template <>
    class vector<MyType> : my_special_vector<MyType> {
        // ...
    };
    template <>
    class vector<MyOtherType> : my_special_vector<MyOtherType> {
        // ...
    };
    // ...etc...
} // namespace std

Jest to całkowicie poprawny kod, w którym użytkownik dostarcza własną implementację wektora dla zestawu typów, w którym najwyraźniej zna bardziej wydajną implementację niż ta znaleziona w (jej kopii) STL.

Ale : Specjalizując się w szablonie, musisz to zrobić w przestrzeni nazw, w której został zadeklarowany. Standard mówi, że vectorjest zadeklarowany w przestrzeni nazw std, więc to właśnie tam użytkownik słusznie spodziewa się specjalizacji typu.

Ten kod działa z niewersjonowaną przestrzenią nazw stdlub z wbudowaną funkcją przestrzeni nazw C ++ 11, ale nie z zastosowaną sztuczką wersjonowania using namespace <nested>, ponieważ ujawnia szczegół implementacji, że prawdziwa przestrzeń nazw, w której vectorzostała zdefiniowana, nie była stdbezpośrednio.

Istnieją inne dziury, przez które można wykryć zagnieżdżoną przestrzeń nazw (patrz komentarze poniżej), ale wbudowane przestrzenie nazw zatapiają je wszystkie. I to wszystko. Niezwykle przydatne w przyszłości, ale AFAIK Standard nie określa wbudowanych nazw przestrzeni nazw dla swojej własnej biblioteki standardowej (choć chciałbym, żeby mi się to nie udało), więc można jej używać tylko do bibliotek stron trzecich, a nie sam standard (chyba że dostawcy kompilatora uzgodnią schemat nazewnictwa).

Marc Mutz - mmutz
źródło
23
+1 za wyjaśnienie, dlaczego using namespace V99;nie działa w przykładzie Stroustrupa.
Steve Jessop,
3
I podobnie, jeśli zacznę zupełnie nową implementację C ++ 21 od zera, to nie chcę być obciążony implementacją wielu starych bzdur std::cxx_11. Nie każdy kompilator zawsze zaimplementuje wszystkie stare wersje bibliotek standardowych, mimo że w tej chwili kuszące jest myślenie, że byłoby bardzo małe obciążenie wymagać od istniejących implementacji pozostawienia starej wersji, gdy dodają nową, ponieważ w rzeczywistości wszystkie one i tak są. Podejrzewam, że to, co standard mógł pożytecznie zrobić, stało się opcjonalne, ale ze standardową nazwą, jeśli istnieje.
Steve Jessop,
46
To nie wszystko. ADL również był przyczyną (ADL nie będzie stosować się do dyrektyw), a także wyszukiwanie nazw. ( using namespace Aw przestrzeni nazw B sprawia, że ​​nazwy w przestrzeni nazw B ukrywają nazwy w przestrzeni nazw A, jeśli szukasz B::name- nie w przypadku wbudowanych przestrzeni nazw).
Johannes Schaub - litb
4
Dlaczego po prostu nie używasz ifdefs do pełnej implementacji wektorowej? Wszystkie implementacje byłyby w jednej przestrzeni nazw, ale tylko jedna z nich zostanie zdefiniowana po przetwarzaniu
wstępnym
6
@ sasha.sochka, ponieważ w tym przypadku nie można użyć innych implementacji. Zostaną usunięte przez preprocesora. Dzięki wbudowanym przestrzeniom nazw możesz użyć dowolnej implementacji, podając w pełni kwalifikowaną nazwę (lub usingsłowo kluczowe).
Wasilij Biryukow
70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (dokument napisany i prowadzony przez Bjarne Stroustrup, który według ciebie powinien być świadomy większości motywacji dla większości funkcji C ++ 11. )

Zgodnie z tym ma umożliwić wersjonowanie w celu zapewnienia zgodności wstecznej. Definiujesz wiele wewnętrznych przestrzeni nazw i tworzysz najnowszą inline. W każdym razie domyślny dla osób, którym nie zależy na wersjonowaniu. Przypuszczam, że najnowsza może być przyszłą lub najnowocześniejszą wersją, która nie jest jeszcze domyślna.

Podany przykład to:

// file V99.h:
inline namespace V99 {
    void f(int);    // does something better than the V98 version
    void f(double); // new feature
    // ...
}

// file V98.h:
namespace V98 {
    void f(int);    // does something
    // ...
}

// file Mine.h:
namespace Mine {
#include "V99.h"
#include "V98.h"
}

#include "Mine.h"
using namespace Mine;
// ...
V98::f(1);  // old version
V99::f(1);  // new version
f(1);       // default version

Nie rozumiem od razu, dlaczego nie umieściłeś using namespace V99;w przestrzeni nazw Mine, ale nie muszę do końca rozumieć przypadku użycia, aby wziąć słowo Bjarne na motywację komitetu.

Steve Jessop
źródło
Czy w rzeczywistości ostatnia f(1)wersja byłaby wywoływana z wbudowanej V99przestrzeni nazw?
Eitan T
1
@EitanT: tak, ponieważ globalna przestrzeń nazw ma using namespace Mine;, a Mineprzestrzeń nazw zawiera wszystko z wbudowanej przestrzeni nazw Mine::V99.
Steve Jessop,
2
@ Walter: usuwasz inlinez pliku V99.hw dołączonej wersji V100.h. Oczywiście modyfikujesz również Mine.hw tym samym czasie, aby dodać dodatkowe dołączenie. Mine.hjest częścią biblioteki, a nie częścią kodu klienta.
Steve Jessop,
5
@walter: nie instalują się V100.h, instalują bibliotekę o nazwie „Mój”. Istnieją 3 pliki nagłówkowe w wersji 99 „moje” - Mine.h, V98.hi V99.h. Są to 4 pliki nagłówkowe w wersji 100 „moje” - Mine.h, V98.h, V99.hi V100.h. Układ plików nagłówkowych jest szczegółem implementacji, który nie ma znaczenia dla użytkowników. Jeśli odkryją jakiś problem ze zgodnością, co oznacza, że ​​muszą używać konkretnie Mine::V98::fcałego lub całego kodu, mogą mieszać wywołania Mine::V98::fze starego kodu ze wywołaniami Mine::fw nowo napisanym kodzie.
Steve Jessop,
2
@Walter Jak wspomniano w innej odpowiedzi, szablony muszą się specjalizować w przestrzeni nazw, w których zostały zadeklarowane, a nie przestrzeni nazw używającej tej, w której zostały zadeklarowane. Choć wygląda to dziwnie, sposób, w jaki się tam robi, pozwala na specjalizację szablonów Mine, zamiast specjalizować się w Mine::V99lub Mine::V98.
Justin Time - Przywróć Monikę
8

Oprócz wszystkich innych odpowiedzi.

Wewnętrzna przestrzeń nazw może być używana do kodowania informacji ABI lub wersji funkcji w symbolach. Z tego powodu są one używane do zapewnienia wstecznej kompatybilności ABI. Wbudowane przestrzenie nazw umożliwiają wstrzykiwanie informacji do zniekształconej nazwy (ABI) bez zmiany interfejsu API, ponieważ wpływają one tylko na nazwę symbolu linkera.

Rozważ ten przykład:

Załóżmy, że piszesz funkcję, Fooktóra odwołuje się do powiedzenia obiektu bari nic nie zwraca.

Powiedz w main.cpp

struct bar;
void Foo(bar& ref);

Jeśli sprawdzisz nazwę swojego symbolu dla tego pliku po skompilowaniu go w obiekt.

$ nm main.o
T__ Z1fooRK6bar 

Nazwa symbolu linkera może się różnić, ale na pewno gdzieś zakoduje nazwę funkcji i typów argumentów.

Teraz może to być barzdefiniowane jako:

struct bar{
   int x;
#ifndef NDEBUG
   int y;
#endif
};

W zależności od typu kompilacji barmoże odnosić się do dwóch różnych typów / układów z tymi samymi symbolami linkera.

Aby temu zapobiec, zawijamy naszą strukturę bardo wbudowanej przestrzeni nazw, gdzie w zależności od typu Kompilacji symbol linkera barbędzie inny.

Więc moglibyśmy napisać:

#ifndef NDEBUG
inline namespace rel { 
#else
inline namespace dbg {
#endif
struct bar{
   int x;
#ifndef NDEBUG
   int y;
#endif
};
}

Teraz, gdy spojrzysz na plik obiektu każdego obiektu, zbudujesz jeden za pomocą wydania, a drugi z flagą debugowania. Przekonasz się, że symbole linkera zawierają również wbudowaną nazwę przestrzeni nazw. W tym przypadku

$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar

Nazwy symboli łącznika mogą być różne.

Zwróć uwagę na obecność reli dbgw nazwach symboli.

Teraz, jeśli spróbujesz połączyć debugowanie z trybem zwalniania lub odwrotnie, otrzymasz błąd linkera jako sprzeczny z błędem środowiska uruchomieniowego.

koder3101
źródło
1
Tak, to ma sens. Jest to więc więcej dla implementatorów bibliotek i tym podobnych.
Walter
3

Właściwie odkryłem inne zastosowanie dla wbudowanych przestrzeni nazw.

Dzięki Qt zyskujesz dodatkowe, miłe funkcje Q_ENUM_NS, które z kolei wymagają, aby otaczająca przestrzeń nazw zawierała meta obiekt, który jest zadeklarowany za pomocą Q_NAMESPACE. Jednak Q_ENUM_NSaby działać, musi być odpowiedni Q_NAMESPACE w tym samym pliku ⁽¹⁾. I może być tylko jeden, lub wystąpią błędy definicji duplikatu. To oznacza, że ​​wszystkie twoje wyliczenia muszą znajdować się w tym samym nagłówku. Fuj

Lub ... możesz użyć wbudowanych przestrzeni nazw. Ukrywanie wyliczeń winline namespacemetodzie powoduje, że meta-obiekty mają różne zniekształcone nazwy, podczas gdy szukanie użytkowników takich jak dodatkowa przestrzeń nazw nie istnieje ⁽²⁾.

Są więc przydatne do dzielenia rzeczy na wiele pod-nazw, które wszystkie wyglądają jak jedna przestrzeń nazw, jeśli z jakiegoś powodu musisz to zrobić. Oczywiście jest to podobne do pisania using namespace innerw zewnętrznej przestrzeni nazw, ale bez naruszenia DRY w przypadku dwukrotnego pisania nazwy wewnętrznej przestrzeni nazw.


  1. To jest naprawdę gorsze niż to; musi być w tym samym zestawie nawiasów klamrowych.

  2. Chyba że spróbujesz uzyskać dostęp do meta-obiektu bez jego pełnego zakwalifikowania, ale meta-obiekt prawie nigdy nie jest używany bezpośrednio.

Mateusz
źródło
Czy możesz naszkicować to za pomocą szkieletu kodu? (idealnie bez wyraźnego odniesienia do Qt). Wszystko brzmi raczej zaangażowane / niejasne.
Walter
Nie łatwo. Powód, dla którego potrzebne są osobne przestrzenie nazw, ma związek ze szczegółami implementacji Qt. TBH, trudno sobie wyobrazić sytuację poza Qt, która miałaby takie same wymagania. Jednak w tym scenariuszu związanym z Qt są cholernie przydatne! Przykład: patrz gist.github.com/mwoehlke-kitware/… lub github.com/Kitware/seal-tk/pull/45 .
Matthew
0

Tak więc, aby podsumować główne punkty, using namespace v99a inline namespacenie były takie same, były było obejście bibliotek wersję przed dedykowanym słowa kluczowego (inline) został wprowadzony w C ++ 11, który Naprawiono problemy związane z użyciem using, zapewniając jednocześnie taką samą funkcjonalność wersjonowania. Używanie using namespaceużywane do powodowania problemów z ADL (chociaż ADL wydaje się teraz podążać za usingdyrektywami), a poza-specjalizacja klasy / funkcji biblioteki itp. Przez użytkownika nie działałaby, gdyby została wykonana poza prawdziwą przestrzenią nazw (której nazwa użytkownik nie wiedziałby i nie powinien wiedzieć, tj. użytkownik musiałby użyć B :: abi_v2 :: zamiast tylko B :: do rozwiązania specjalizacji).

//library code
namespace B { //library name the user knows
    namespace A { //ABI version the user doesn't know about
        template<class T> class myclass{int a;};
    }
    using namespace A; //pre inline-namespace versioning trick
} 

// user code
namespace B { //user thinks the library uses this namespace
    template<> class myclass<int> {};
}

Wyświetli się ostrzeżenie dotyczące analizy statycznej first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]. Ale jeśli utworzysz przestrzeń nazw A w linii, kompilator poprawnie rozwiąże specjalizację. Chociaż w przypadku rozszerzeń C ++ 11 problem zniknął.

Definicje poza linią nie są rozwiązywane podczas używania using; należy je zadeklarować w zagnieżdżonym / nie zagnieżdżonym bloku przestrzeni nazw rozszerzeń (co oznacza, że ​​użytkownik musi ponownie znać wersję ABI, jeśli z jakiegokolwiek powodu zezwolono im na zapewnienie własnej implementacji funkcji).

#include <iostream>
namespace A {
    namespace B{
        int a;
        int func(int a);
        template<class T> class myclass{int a;};
        class C;
        extern int d;
    } 
    using namespace B;
} 
int A::d = 3; //No member named 'd' in namespace A
class A::C {int a;}; //no class named 'C' in namespace 'A' 
template<> class A::myclass<int> {}; // works; specialisation is not an out-of-line definition of a declaration
int A::func(int a){return a;}; //out-of-line definition of 'func' does not match any declaration in namespace 'A'
namespace A { int func(int a){return a;};} //works
int main() {
    A::a =1; // works; not an out-of-line definition
}

Problem znika, gdy B jest wbudowany.

Inne funkcjonalne inlineprzestrzenie nazw mają na celu umożliwienie autorowi biblioteki dostarczenia przezroczystej aktualizacji biblioteki 1) bez zmuszania użytkownika do zmiany kodu z nową nazwą przestrzeni nazw i 2) zapobiegania brakowi szczegółowości oraz 3) zapewniania abstrakcji szczegółów niezwiązanych z API, podczas gdy 4) daje taką samą korzystną diagnostykę i zachowanie linkera, jaką zapewniłoby użycie nieliniowej przestrzeni nazw. Powiedzmy, że używasz biblioteki:

namespace library {
    inline namespace abi_v1 {
        class foo {
        } 
    }
}

Pozwala użytkownikowi dzwonić library::foobez konieczności znajomości lub dołączenia wersji ABI do dokumentacji, która wygląda na czystszą. Używanie library::abiverison129389123::foowyglądałoby na brudne.

Po dokonaniu aktualizacji foo, tj. Dodaniu nowego elementu do klasy, nie wpłynie to na istniejące programy na poziomie interfejsu API, ponieważ nie będą one już korzystać z tego elementu ORAZ zmiana nazwy wbudowanej przestrzeni nazw nic nie zmieni na poziomie interfejsu API ponieważ library::foonadal będzie działać.

namespace library {
    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}

Jednak w przypadku programów, które się z nim łączą, ponieważ wbudowana nazwa przestrzeni nazw jest zniekształcona w nazwach symboli, tak jak zwykła przestrzeń nazw, zmiana nie będzie przezroczysta dla linkera. Dlatego jeśli aplikacja nie zostanie ponownie skompilowana, ale zostanie połączona z nową wersją biblioteki, abi_v1wyświetli się błąd, który nie został znaleziony, zamiast łączenia się, a następnie spowodowania tajemniczego błędu logicznego w czasie wykonywania z powodu niezgodności ABI. Dodanie nowego elementu spowoduje zgodność ABI ze względu na zmianę definicji typu, nawet jeśli nie wpłynie to na program w czasie kompilacji (poziom API).

W tym scenariuszu:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}

Podobnie jak w przypadku korzystania z 2 nieliniowych przestrzeni nazw, umożliwia połączenie nowej wersji biblioteki bez konieczności ponownej kompilacji aplikacji, ponieważ abi_v1zostanie ona zniekształcona w jednym z globalnych symboli i użyje poprawnej (starej) definicji typu. Ponowna kompilacja aplikacji spowodowałaby jednak rozpoznanie referencji library::abi_v2.

Używanie using namespacejest mniej funkcjonalne niż używanie inline(ponieważ definicje poza linią nie rozwiązują), ale zapewnia te same 4 zalety, co powyżej. Ale prawdziwe pytanie brzmi: po co nadal stosować obejście, skoro istnieje teraz specjalne słowo kluczowe do tego celu. Jest to lepsza praktyka, mniej gadatliwa (trzeba zmienić 1 linię kodu zamiast 2) i wyjaśnia intencję.

Lewis Kelsey
źródło