Namensräume
Bestimmte Namen für Funktionen und Klassen bieten sich einfach an und werden daher immer wieder verwendet. Um eine Kollision zu vermeiden (speziell bei der Verwendung verschiedener Bibliotheken, die man nicht immer abändern kann und will), kann man C++-Code in verschiedene Namensräume einpacken und so Eindeutigkeit schaffen. Das Thema ist heute mal wieder auf zwei Videos aufgeteilt, da es zu viel für ein einzelnes ist.
Videos
Namespaces allgemein
Anonmous und inline Namespaces
Code
Namensräume
#include <iostream>
namespace N1
{
void print()
{
std::cout << "N1::print()\n";
}
}
namespace N2
{
void print()
{
std::cout << "N2::print()\n";
}
}
void callPrint()
{
std::cout << "Global -> ";
N2::print();
}
namespace N1
{
void callPrint()
{
print();
}
namespace Inner
{
void callFromInner()
{
std::cout << "Inner -> ";
print();
}
}
void callInner()
{
std::cout << "N1 -> ";
Inner::callFromInner();
}
void callViaGlobal()
{
std::cout << "N1 -> ";
::callPrint();
}
}
namespace NAlias = N1;
using namespace N2;
int main()
{
N1::print();
N2::print();
N1::callPrint();
N1::Inner::callFromInner();
N1::callInner();
N1::callViaGlobal();
NAlias::print();
print();
}
Anonymous Namespaces
namespace {
int x;
}
int main()
{
x = 10;
}
inline Namespaces
#include <iostream>
namespace Outer
{
namespace Inner
{
void print()
{
std::cerr << "Outer::Inner::print()\n";
}
}
inline namespace V2
{
void print()
{
std::cerr << "Outer::V2::print()\n";
}
}
}
namespace OldOuter
{
namespace Inner
{
void print()
{
std::cerr << "OldOuter::Inner::print()\n";
}
}
using namespace Inner;
}
int main()
{
Outer::print();
Outer::Inner::print();
OldOuter::print();
OldOuter::Inner::print();
}
Erklärung
Ein Namespace ist erstmal ganz einfach definiert: namespace Name { }. Alles,
was innerhalb der geschweiften Klammern steht gehört zu dem Namespace. Um große
Namespaces effizient verwalten und auf mehrere Dateien verteilen zu können, kann
man sie jederzeit wieder öffnen. Eine weitere namespace-Anweisung mit dem
gleichen Namen fügt also einfach nur zusätzliche Dinge in den Namensraum ein.
Namensräume können ineinander verschachtelt werden. Auf diese Art und Weise kann
man bspw. alle Elemente einer größeren Bibliothek in einen großen Namensraum
zusammenfassen und dann weiter thematisch unterteilen. Ein Beispiel für die
Anwendung dieses Prinzips findet sich in der Boost-Bibliothek: alles liegt im
Namensraum boost und wird dann dort weiter unterteilt je nach Unterbibliothek.
Will man auf einen Namen innerhalb eines Namespace zugreifen, so hat man mehrere
Möglichkeiten: man kann ihn ausgehend vom aktuellen Namespace (in welchem man
sich selbst befindet) qualifizieren. Im Beispiel oben ist das bspw. in Zeile 44
oder 64 zu sehen. Zeile 44 wechselt vom Namespace N1 in den inneren Namespace
Inner und sucht dort nach dem Namen callFromInner. Der vollständige Name,
nach dem also gesucht wird, ist ::N1::Inner::callFromInner. Angegeben werden
muss natürlich nur der Teil ab dem Namespace, in dem man sich selbst befindet.
Vergleichbar ist Zeile 64: dort wird der gleiche Name gesucht, jetzt allerdings
aus dem globalen, namenlosen Namespace. Daher muss N1 diesmal mit angegeben
werden.
Will man aus einem Namespace heraus auf umgebende Namen zugreifen, dann gibt es
zwei Möglichkeiten: standardmäßig sucht der Compiler, wenn er einen Namen im
eigenen Namespace nicht finden kann, so lang nach außen, bis er entweder was
passendes findet oder es nicht mehr weitergeht (was dann zu einem Fehler führt).
Für den – seltenen – Fall, dass ein innerer Name (bspw. im eigenen umgebenden
Namespace) einen äußeren Namen verdecken würde, man aber auf den äußeren
zugreifen will, gibt es die Möglichkeit mittels :: direkt ganz nach außen zu
gehen. Zeile 50 demonstriert das vorgehen. Die globale Funktion callPrint()
wird von dort aus gesehen durch die Funktion N1::callPrint() verdeckt und ist
ohne weiteres nicht erreichbar. Durch den Aufruf ::callPrint() können wir dem
Compiler allerdings mitteilen, dass wir gern direkt ganz nach außen gehen würden
und von dort aus suchen.
Will man sich nun ein wenig Tipparbeit sparen und nicht immer die kompletten
Namensräume (oder gar verschachtelte Ketten davon) hinschreiben wollen. Hier
gibt es zwei Möglichkeiten abzukürzen: einerseits kann man mittels
namespace X = Y; einen Alias für namens X für den Namensraum Y vergeben,
so dass immer, wenn man Y schreiben müsste, man stattdessen nur X schreiben
braucht (unter der Annahme, das X und Y deutlich unterschiedlich lang sind).
Andererseits kann man Namen eines Namensraumes komplett in einen anderen
importieren, indem man die Anweisung using namespace X; verwendet. Danach sind
alle Namen aus X in dem Namensraum, in dem man diese using-Anweisung
verwendet hat, verfügbar. Beide Varianten sollte man sparsam einsetzen (speziell
bei using mischt man ja wieder Namensräume und fängt sich potentiell Probleme
mit Namenskollisionen ein). Beides sollte man niemals in Header-Dateien
verwenden (Aliases vielleicht, aber auch nur, wenn man ganz genau weiß, was man
tut.)
Anonymous Namespaces
Deutlich seltener, als die normalen Namespaces, braucht man anonyme Namensräume,
also Namensräume ohne Namen. Klingt nach einem eigenwilligen Konzept, kann aber
in ganz bestimmten Situationen hilfreich sein. Beispiel 2 zeigt so eine
Anwendung: die Variable x soll nur innerhalb der Datei global verfügbar sein
(hier könnte man sich bspw. interne Helferfunktionen oder ähnliches vorstellen).
Wenn wir diese nun einfach so in die Datei schreiben und das vielleicht woanders
nochmal tun, dann stört das den Compiler nicht. Der Linker hingegen beschwert
sich, weil er ein Symbol gleichen Namens mehrfach definiert findet. Das können
wir mit einem anonymen Namensraum beheben: hier vergibt der Compiler einen
generierten, garantiert eindeutigen Namen und importiert diesen direkt mittels
using. So ist alles aus dem Namespace genauso verfügbar, als würde der nicht
existieren, dafür sind aber alle Namen darin garantiert eindeutig und führen
nicht zu einer Kollision beim Linken. inline-Namespaces
Inline-Namespaces lösen ein Problem, was nur die Entwickler von Bibliotheken
haben dürften. Wenn man eine Bibliothek über lange Zeit pflegt und
weiterentwickelt, dann möchte man möglicherweise auch deren Schnittstelle
weiterentwickeln. Gleichzeitig möchte man (speziell bei kommerziellen
Bibliotheken) aber den Nutzern die Möglichkeit geben, sich auf bestimmte
Schnittstellen zu verlassen. Zu diesem Zweck kann man Schnittstellen
versionieren, indem man sie in verschiedene Namensräume einpackt (oben im
Beispiel 3 bspw. die erste Version im Namensraum Inner, die zweite in V2.)
Den jeweils aktuellsten Namensraum importiert man dann in dem umgebenden
Namespace der Bibliothek und macht ihn so für alle als Schnittstelle direkt
sichtbar. Wer sich einfach auf die aktuellste Variante verlassen will, der kann
direkt mittels Outer:: auf die Funktionen zugreifen und wird auf die jeweils
aktuellste Variante geleitet. Wer eine bestimmte Version braucht, kann diese
mittels Outer::Version:: qualifizieren und sich darauf verlassen, dass sich
daran nichts ändert (wenn der Anbieter der Bibliothek das verspricht).
Die beiden Varianten in Beispiel 3 verhalten sich von außen auf den ersten Blick identisch. Es gibt allerdings einen Randfall (für alle, die das schonmal nachlesen wollen: es geht um die Spezialisierung von Templates in Namensräumen), für den das zweite Beispiel fehlschlägt und den Nutzer zwingen würde, zu wissen um welchen genauen inneren Namensraum es gerade geht. Mit C++11 und den inline-Namespaces wurde hier Abhilfe geschaffen. Auch der Randfall muss hier korrekt funktionieren.