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

Quellcode

Namespaces

#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, namenlos 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 sucher 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() würde von dort aus gesehen durch die Funktion N1::callPrint() verdeckt und wäre 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.