W lekcji 2.9 -- Kolizje nazewnictwa i wprowadzenie do przestrzeni nazw, wprowadziliśmy koncepcję naming collisions i namespaces. Dla przypomnienia, kolizja nazewnictwa ma miejsce, gdy do tego samego zakresu wprowadzane są dwa identyczne identyfikatory, a kompilator nie jest w stanie rozróżnić, którego z nich użyć. Kiedy tak się stanie, kompilator lub linker zgłosi błąd, ponieważ nie ma wystarczającej ilości informacji, aby rozwiązać niejednoznaczność.
Kluczowa informacja
W miarę powiększania się programów wzrasta liczba identyfikatorów, co z kolei powoduje znaczny wzrost prawdopodobieństwa wystąpienia kolizji nazewnictwa. Ponieważ każda nazwa w danym zakresie może potencjalnie kolidować z każdą inną nazwą w tym samym zakresie, liniowy wzrost identyfikatorów spowoduje wykładniczy wzrost potencjalnych kolizji! Jest to jeden z kluczowych powodów definiowania identyfikatorów w możliwie najmniejszym zakresie.
Przyjrzyjmy się ponownie przykładowi kolizji nazewnictwa, a następnie pokażmy, jak możemy ulepszyć sytuację za pomocą przestrzeni nazw. W poniższym przykładzie foo.cpp i goo.cpp to pliki źródłowe zawierające funkcje, które wykonują różne zadania, ale mają tę samą nazwę i parametry.
foo.cpp:
// This doSomething() adds the value of its parameters
int doSomething(int x, int y)
{
return x + y;
}goo.cpp:
// This doSomething() subtracts the value of its parameters
int doSomething(int x, int y)
{
return x - y;
}main.cpp:
#include <iostream>
int doSomething(int x, int y); // forward declaration for doSomething
int main()
{
std::cout << doSomething(4, 3) << '\n'; // which doSomething will we get?
return 0;
}Jeśli ten projekt zawiera tylko foo.cpp lub goo.cpp (ale nie oba), skompiluje się i uruchomi bez żadnych problemów. Jednak kompilując oba do tego samego programu, wprowadziliśmy teraz dwie różne funkcje o tej samej nazwie i parametrach do tego samego zasięgu (zakresu globalnego), co powoduje kolizję nazewnictwa. W rezultacie linker zgłosi błąd:
goo.cpp:3: multiple definition of `doSomething(int, int)'; foo.cpp:3: first defined here
Zauważ, że ten błąd pojawia się w momencie redefinicji, więc nie ma znaczenia, czy funkcja doSomething zostanie kiedykolwiek wywołana.
Jednym ze sposobów rozwiązania tego problemu byłaby zmiana nazwy jednej z funkcji, tak aby nazwy nie kolidowały już ze sobą. Wymagałoby to jednak również zmiany nazw wszystkich wywołań funkcji, co może być uciążliwe i obarczone błędami. Lepszym sposobem uniknięcia kolizji jest umieszczenie funkcji we własnych przestrzeniach nazw. Z tego powodu standardowa biblioteka została przeniesiona do std .
Definiowanie własnych przestrzeni nazw
C++ pozwala nam definiować własne przestrzenie nazw za pomocą słowa kluczowego namespace . Przestrzenie nazw tworzone we własnych programach są potocznie nazywane przestrzeniami nazw zdefiniowanymi przez użytkownika (chociaż dokładniej byłoby nazwać je przestrzeniami nazw zdefiniowanymi przez program).
Składnia przestrzeni nazw jest następująca:
namespace NamespaceIdentifier
{
// content of namespace here
}
Zaczynamy od słowa kluczowego namespace , po którym następuje identyfikator przestrzeni nazw, a następnie nawiasy klamrowe z zawartością przestrzeni nazw.
Historycznie nazwy przestrzeni nazw nie były pisane wielką literą i wiele przewodników stylistycznych nadal zaleca tę konwencję.
Dla zaawansowanych czytelników
Kilka powodów, dla których warto preferować nazwy przestrzeni nazw rozpoczynające się od dużej litery:
- Przyjętą konwencją jest nadawanie nazw typom zdefiniowanym przez program zaczynając od dużej litery. Stosowanie tej samej konwencji w przypadku przestrzeni nazw zdefiniowanych przez program jest spójne (szczególnie w przypadku używania nazw kwalifikowanych, takich jak as
Foo::x, gdzieFoomoże być przestrzenią nazw lub typem klasy). - Pomaga zapobiegać kolizjom nazewnictwa z innymi nazwami dostarczanymi przez system lub bibliotekami, pisanymi małymi literami.
- Dokument standardów C++20 używa tego stylu.
- Dokument zawierający wytyczne C++ Core używa tego stylu.
Zalecamy jednak rozpoczynanie nazw przestrzeni nazw wielką literą styl powinien być postrzegany jako akceptowalny.
Przestrzeń nazw musi być zdefiniowana albo w zakresie globalnym, albo w innej przestrzeni nazw. Podobnie jak treść funkcji, zawartość przestrzeni nazw jest konwencjonalnie wcięta o jeden poziom. Czasami po nawiasie zamykającym przestrzeni nazw może pojawić się opcjonalny średnik.
Oto przykład plików z poprzedniego przykładu przepisanych przy użyciu. przestrzenie nazw:
foo.cpp:
namespace Foo // define a namespace named Foo
{
// This doSomething() belongs to namespace Foo
int doSomething(int x, int y)
{
return x + y;
}
}goo.cpp:
namespace Goo // define a namespace named Goo
{
// This doSomething() belongs to namespace Goo
int doSomething(int x, int y)
{
return x - y;
}
}Teraz doSomething() wewnątrz foo.cpp znajduje się w przestrzeni nazw Foo przestrzeni nazw, a doSomething() wewnątrz goo.cpp znajduje się w przestrzeni nazw Goo przestrzeni nazw. Zobaczmy, co się stanie, gdy przekompilujemy nasz program.
main.cpp:
int doSomething(int x, int y); // forward declaration for doSomething
int main()
{
std::cout << doSomething(4, 3) << '\n'; // which doSomething will we get?
return 0;
}Odpowiedź jest taka, że dostajemy kolejny błąd!
ConsoleApplication1.obj : error LNK2019: unresolved external symbol "int __cdecl doSomething(int,int)" (?doSomething@@YAHHH@Z) referenced in function _main
W tym przypadku kompilator spełnił oczekiwania (naszą deklaracją forward), ale linker nie mógł znaleźć definicji doSomething w globalnej przestrzeni nazw. Dzieje się tak, ponieważ obie nasze wersje doSomething nie znajdują się już w globalnej przestrzeni nazw! Znajdują się one teraz w zakresie odpowiednich przestrzeni nazw!
Istnieją dwa różne sposoby poinformowania kompilatora, której wersji doSomething() ma użyć, poprzez scope resolution operator lub poprzez using statements (co omówimy w dalszej lekcji w tym rozdziale).
W kolejnych przykładach dla ułatwienia zwiniemy nasze przykłady do rozwiązania jednoplikowego czytanie.
Dostęp do przestrzeni nazw za pomocą operatora rozpoznawania zakresu (::)
Najlepszym sposobem poinformowania kompilatora, aby szukał identyfikatora w określonej przestrzeni nazw, jest użycie operator rozpoznawania zakresu (::). Operator rozpoznawania zakresu mówi kompilatorowi, że identyfikatora określonego przez prawy operand należy szukać w zakresie lewego operandu.
Oto przykład użycia operatora rozpoznawania zakresu w celu poinformowania kompilatora, że jawnie chcemy użyć wersji doSomething() która znajduje się w Foo przestrzeń nazw:
#include <iostream>
namespace Foo // define a namespace named Foo
{
// This doSomething() belongs to namespace Foo
int doSomething(int x, int y)
{
return x + y;
}
}
namespace Goo // define a namespace named Goo
{
// This doSomething() belongs to namespace Goo
int doSomething(int x, int y)
{
return x - y;
}
}
int main()
{
std::cout << Foo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Foo
return 0;
}Daje to oczekiwany wynik:
7
Gdybyśmy chcieli użyć wersji doSomething() która żyje w Goo zamiast tego:
#include <iostream>
namespace Foo // define a namespace named Foo
{
// This doSomething() belongs to namespace Foo
int doSomething(int x, int y)
{
return x + y;
}
}
namespace Goo // define a namespace named Goo
{
// This doSomething() belongs to namespace Goo
int doSomething(int x, int y)
{
return x - y;
}
}
int main()
{
std::cout << Goo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Goo
return 0;
}Daje to wynik:
1
Operator rozpoznawania zakresu jest świetny, ponieważ pozwala nam jawnie wybrać przestrzeń nazw, w której chcemy szukać, więc nie ma potencjalnej dwuznaczności. Możemy nawet wykonać następujące czynności:
#include <iostream>
namespace Foo // define a namespace named Foo
{
// This doSomething() belongs to namespace Foo
int doSomething(int x, int y)
{
return x + y;
}
}
namespace Goo // define a namespace named Goo
{
// This doSomething() belongs to namespace Goo
int doSomething(int x, int y)
{
return x - y;
}
}
int main()
{
std::cout << Foo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Foo
std::cout << Goo::doSomething(4, 3) << '\n'; // use the doSomething() that exists in namespace Goo
return 0;
}Daje to wynik:
7 1
Użycie operatora rozpoznawania zakresu bez przedrostka nazwy
Operatora rozpoznawania zakresu można również użyć przed identyfikatorem bez podawania nazwy przestrzeni nazw (np. ::doSomething). W takim przypadku identyfikator (np. doSomething) szukany jest w globalnej przestrzeni nazw.
#include <iostream>
void print() // this print() lives in the global namespace
{
std::cout << " there\n";
}
namespace Foo
{
void print() // this print() lives in the Foo namespace
{
std::cout << "Hello";
}
}
int main()
{
Foo::print(); // call print() in Foo namespace
::print(); // call print() in global namespace (same as just calling print() in this case)
return 0;
}W powyższym przykładzie funkcja ::print() zachowuje się tak samo, jak gdybyśmy wywołali print() bez rozpoznawania zakresu, zatem użycie operatora rozpoznawania zakresu jest w tym przypadku zbędne. Ale następny przykład pokaże przypadek, w którym przydatny może być operator rozpoznawania zakresu bez przestrzeni nazw.
Rozpoznawanie identyfikatora z poziomu przestrzeni nazw
Jeśli używany jest identyfikator wewnątrz przestrzeni nazw i nie podano rozpoznawania zakresu, kompilator najpierw spróbuje znaleźć pasującą deklarację w tej samej przestrzeni nazw. Jeśli nie zostanie znaleziony pasujący identyfikator, kompilator sprawdzi kolejno każdą zawierającą przestrzeń nazw, aby sprawdzić, czy zostanie znalezione dopasowanie, przy czym globalna przestrzeń nazw będzie sprawdzana jako ostatnia.
#include <iostream>
void print() // this print() lives in the global namespace
{
std::cout << " there\n";
}
namespace Foo
{
void print() // this print() lives in the Foo namespace
{
std::cout << "Hello";
}
void printHelloThere()
{
print(); // calls print() in Foo namespace
::print(); // calls print() in global namespace
}
}
int main()
{
Foo::printHelloThere();
return 0;
}Wypisuje:
Hello there
W powyższym przykładzie print() jest wywoływana bez podawania rozdzielczości zakresu. Ponieważ to użycie print() znajduje się w przestrzeni nazw Foo , kompilator najpierw sprawdzi, czy można znaleźć deklarację dla Foo::print() . Ponieważ taki istnieje, Foo::print() .
Jeśli Foo::print() nie został znaleziony, kompilator sprawdziłby zawierającą przestrzeń nazw (w tym przypadku globalną przestrzeń nazw), aby sprawdzić, czy może pasować print() tam.
Zauważ, że używamy również operatora rozpoznawania zakresu bez przestrzeni nazw (::print()), aby jawnie wywołać globalną wersję print().
Prześlij dalej deklarację treści w przestrzeni nazw
W lekcji 2.11 — Pliki nagłówkowe, omówiliśmy, w jaki sposób możemy używać plików nagłówkowych do propagowania deklaracji przekazywania. W przypadku identyfikatorów wewnątrz przestrzeni nazw te deklaracje forward również muszą znajdować się w tej samej przestrzeni nazw:
add.h
#ifndef ADD_H
#define ADD_H
namespace BasicMath
{
// function add() is part of namespace BasicMath
int add(int x, int y);
}
#endifadd.cpp
#include "add.h"
namespace BasicMath
{
// define the function add() inside namespace BasicMath
int add(int x, int y)
{
return x + y;
}
}main.cpp
#include "add.h" // for BasicMath::add()
#include <iostream>
int main()
{
std::cout << BasicMath::add(4, 3) << '\n';
return 0;
}Jeśli deklaracja forward dla add() nie została umieszczona w przestrzeni nazw BasicMath, wówczas add() zostałaby zamiast tego zadeklarowana w globalnej przestrzeni nazw, a kompilator narzekałby, że nie widział deklaracji wywołania BasicMath::add(4, 3). Jeśli definicja funkcji add() nie znajdowała się w przestrzeni nazw BasicMath, linker narzekałby, że nie mógł znaleźć pasującej definicji dla wywołania BasicMath::add(4, 3).
Dozwolonych jest wiele bloków przestrzeni nazw
Deklaracja bloków przestrzeni nazw w wielu lokalizacjach (w wielu plikach lub w wielu miejscach w tym samym pliku) jest legalna. Wszystkie deklaracje w przestrzeni nazw są uważane za część przestrzeni nazw.
circle.h:
#ifndef CIRCLE_H
#define CIRCLE_H
namespace BasicMath
{
constexpr double pi{ 3.14 };
}
#endifgrowth.h:
#ifndef GROWTH_H
#define GROWTH_H
namespace BasicMath
{
// the constant e is also part of namespace BasicMath
constexpr double e{ 2.7 };
}
#endifmain.cpp:
#include "circle.h" // for BasicMath::pi
#include "growth.h" // for BasicMath::e
#include <iostream>
int main()
{
std::cout << BasicMath::pi << '\n';
std::cout << BasicMath::e << '\n';
return 0;
}Działa to dokładnie tak, jak można się spodziewać:
3.14 2.7
Biblioteka standardowa szeroko wykorzystuje tę funkcję, ponieważ każdy plik nagłówkowy biblioteki standardowej zawiera swoje deklaracje w namespace std bloku zawartym w tym pliku nagłówkowym. W przeciwnym razie cała biblioteka standardowa musiałaby być zdefiniowana w jednym pliku nagłówkowym!
Zauważ, że ta możliwość oznacza również, że możesz dodać własną funkcjonalność do std przestrzeni nazw. Takie postępowanie powoduje w większości przypadków niezdefiniowane zachowanie, ponieważ std przestrzeń nazw ma specjalną regułę zabraniającą rozszerzania z kodu użytkownika.
Ostrzeżenie
Nie dodawaj niestandardowych funkcji do standardowej przestrzeni nazw.
Zagnieżdżone przestrzenie nazw
Przestrzenie nazw można zagnieżdżać w innych przestrzeniach nazw. Na przykład:
#include <iostream>
namespace Foo
{
namespace Goo // Goo is a namespace inside the Foo namespace
{
int add(int x, int y)
{
return x + y;
}
}
}
int main()
{
std::cout << Foo::Goo::add(1, 2) << '\n';
return 0;
}Zauważ, że ponieważ przestrzeń nazw Goo znajduje się wewnątrz przestrzeni nazw Foo, uzyskujemy dostęp add jak Foo::Goo::add.
Od C++17 zagnieżdżone przestrzenie nazw można również zadeklarować w ten sposób:
#include <iostream>
namespace Foo::Goo // Goo is a namespace inside the Foo namespace (C++17 style)
{
int add(int x, int y)
{
return x + y;
}
}
int main()
{
std::cout << Foo::Goo::add(1, 2) << '\n';
return 0;
}Jest to równoważne z poprzednim przykładem.
Jeśli później będziesz musiał dodać deklaracje do Foo przestrzeń nazw (tylko) możesz w tym celu zdefiniować oddzielną Foo przestrzeń nazw:
#include <iostream>
namespace Foo::Goo // Goo is a namespace inside the Foo namespace (C++17 style)
{
int add(int x, int y)
{
return x + y;
}
}
namespace Foo
{
void someFcn() {} // This function is in Foo only
}
int main()
{
std::cout << Foo::Goo::add(1, 2) << '\n';
return 0;
}To, czy zachowasz oddzielną Foo::Goo definicję, czy zagnieżdżenie Goo wewnątrz Foo jest wyborem stylistycznym.
Aliasy przestrzeni nazw
Ponieważ wpisanie kwalifikowanej nazwy zmiennej lub funkcji wewnątrz zagnieżdżona przestrzeń nazw może być kłopotliwa, C++ pozwala na tworzenie aliasów przestrzeni nazw, które pozwalają nam tymczasowo skrócić długą sekwencję przestrzeni nazw do czegoś krótszego:
#include <iostream>
namespace Foo::Goo
{
int add(int x, int y)
{
return x + y;
}
}
int main()
{
namespace Active = Foo::Goo; // active now refers to Foo::Goo
std::cout << Active::add(1, 2) << '\n'; // This is really Foo::Goo::add()
return 0;
} // The Active alias ends hereJedna niezła zaleta aliasów przestrzeni nazw: jeśli kiedykolwiek będziesz chciał przenieść funkcjonalność Foo::Goo w inne miejsce, możesz po prostu zaktualizować alias Active , aby odzwierciedlić nowe miejsce docelowe, zamiast konieczności znajdowania/zastępowania każdego wystąpienia Foo::Goo.
#include <iostream>
namespace Foo::Goo
{
}
namespace V2
{
int add(int x, int y)
{
return x + y;
}
}
int main()
{
namespace Active = V2; // active now refers to V2
std::cout << Active::add(1, 2) << '\n'; // We don't have to change this
return 0;
}Jak korzystać z przestrzeni nazw
Warto zauważyć, że przestrzenie nazw w C++ nie zostały pierwotnie zaprojektowane jako sposób na implementację hierarchii informacji - zostały zaprojektowane przede wszystkim jako mechanizm zapobiegania kolizjom nazewnictwa. Jako dowód można zauważyć, że cała biblioteka standardowa znajduje się w jednej przestrzeni nazw najwyższego poziomu std. Nowsze standardowe funkcje bibliotek, które wprowadzają wiele nazw, zaczęto używać zagnieżdżonych przestrzeni nazw (np. std::ranges), aby uniknąć kolizji nazw w std .
- Małe aplikacje tworzone na własny użytek zazwyczaj nie muszą być umieszczane w przestrzeniach nazw. Jednak w przypadku większych projektów osobistych, które obejmują wiele bibliotek stron trzecich, zastosowanie przestrzeni nazw w kodzie może pomóc zapobiec kolizjom nazw z bibliotekami, które nie mają prawidłowej przestrzeni nazw.
Nota autora
Przykłady w tych samouczkach zazwyczaj nie będą miały przestrzeni nazw, chyba że zilustrujemy coś konkretnego na temat przestrzeni nazw, aby pomóc zachować zwięzłość przykładów.
- Każdy kod, który będzie dystrybuowany innym, powinien zdecydowanie mieć przestrzeń nazw, aby zapobiec konfliktom z kodem, z którym jest zintegrowany. Często wystarczy pojedyncza przestrzeń nazw najwyższego poziomu (np.
Foologger). Dodatkową zaletą jest to, że umieszczenie kodu biblioteki w przestrzeni nazw pozwala użytkownikowi zobaczyć zawartość biblioteki za pomocą funkcji autouzupełniania i sugestii edytora (np. jeśli wpiszeszFoologger, autouzupełnianie wyświetli wszystkie nazwy znajdujące się w przestrzeniFoologger). - W organizacjach wielozespołowych często stosuje się dwupoziomowe lub nawet trzypoziomowe przestrzenie nazw, aby zapobiec konfliktom nazewnictwa między kodem generowanym przez różne zespoły. Często przybierają one formę jednej z następujące:
- Projekt lub biblioteka :: moduł (np.
Foologger::Lang) - Firma lub organizacja :: projekt lub biblioteka (np.
Foosoft::Foologger) - Firma lub organizacja :: projekt lub biblioteka :: moduł (np.
Foosoft::Foologger::Lang)
Użycie przestrzeni nazw na poziomie modułu może pomóc w oddzieleniu kodu, który może nadawać się do ponownego wykorzystania później, od kodu specyficznego dla aplikacji, którego nie będzie można ponownie wykorzystać. Na przykład funkcje fizyczne i matematyczne mogą zostać umieszczone w jednej przestrzeni nazw (np. Math::). Funkcje językowe i lokalizacyjne w innym (np. Lang::). Jednak w tym celu można również użyć struktur katalogów (z kodem specyficznym dla aplikacji w drzewie katalogów projektu i kodem do ponownego użycia w oddzielnym drzewie katalogów współdzielonych).
Ogólnie rzecz biorąc, należy unikać głęboko zagnieżdżonych przestrzeni nazw (więcej niż 3 poziomy).
Powiązana treść
C++ zapewnia inne przydatne funkcje przestrzeni nazw. Nienazwane przestrzenie nazw i wbudowane przestrzenie nazw omówimy w dalszej części tego rozdziału, podczas lekcji >7.14 -- Nienazwane i wbudowane przestrzenie nazw.

