C++ Tutorial

Namensräume

Ein Namensraum kapselt alle in ihm enthaltenen Namen, sodass unterschiedliche Namensräume gleichnamige Daten und Funktionen enthalten können, ohne dass dies zu einem Fehler führt.

Im Beispiel wird eine Variable var und eine Funktion Func definiert. Auf beide wird in main() zugegriffen.

1: // Definitionen
2: int var;
3: void Func()
4: {...}
5: // main() Funktion
6: int main()
7: {
8:    auto var = 10;
9:    Func()
10:   ...
11: }

Angenommen das Programm wird erweitert und dazu soll eine Bibliothek eingesetzt werden. Zu dieser Bibliothek gehört u.a. eine Header-Datei lib.h, die auszugsweise unten dargestellt ist. In dieser Header-Datei sind ebenfalls eine Funktion Func und eine Variable var definiert.

1: // Header-Datei lib.h der Bibliothek
2: short var;
3: void Func();
4: ...

Was folgt, lässt sich erahnen. Der Compiler bzw. der Linker wird sich beim Übersetzen des Programms beschweren, dass einige Symbole mehrfach definiert sind.

1: #include "lib.h"
2: // Definitionen
3: int var;        // Doppelte Definitionen, da beide
4: void Func()     // Namen auch in lib.h definiert sind
5: {...}
6: // main() Funktion
7: int main()
8: {
9:    auto var = 10;
10:    Func();
11:    ...
12: }

Und für solche Fälle wurde der Namensraum geschaffen.

Definition eines Namensraums

Die Syntax zur Definition eines Namensraums lautet:

namespace [NName]
{
    ...   // Definitionen und Deklarationen
}

NName ist der Name des Namensraums. Und alle in einem Namensraum stehenden Definitionen und Deklarationen sind nur innerhalb dieses Namensraums gültig! Gleichnamige Definitionen und Deklarationen in unterschiedlichen Namensräumen erzeugen somit keinen Namenskonflikt mehr.

Um die Namenskonflikte in unserem Beispiel mithilfe von Namensräumen aufzulösen, werden die Definitionen und Deklarationen der Anwendung in einen Namensraum gelegt. Da die Definitionen von var und Func nun in verschiedenen Namensräumen liegen, kommt es zu keinen Namenskonflikten mehr.

1: #include "lib.h"
2: namespace cppkurs
3: {
4:    int var;
5:    void Func()
6:    {...}
7: }
8: int main()
9: {...}

Wird ein bestehender Namensraum erneut definiert, wird der Namensraum entsprechend erweitert.

Zugriff auf Namensräume

Eine Möglichkeit ist, beim Zugriff zuerst den Namensraum, dann den Zugriffsoperator :: und anschließend das Datum bzw. die Funktion anzugeben.

1: #include "lib.h"
2: namespace cppkurs
3: {
4:    int var;       // Eigene Definitionen im Namensraum
5:    void Func()
6:    {...}
7: }
8: // Zugriff auf var aus lib
9: var = 10;
10: // Func aus dem Namensraum cppkurs
11: cppkurs::Func();
12: // Zugriff auf var im Namensraum cppkurs
13: cppkurs::var = 33;

Eine weitere Möglichkeit ist, einen Namen aus einem Namensraum einzublenden. Dies erfolgt mittels des Schlüsselworts using.

using NName::DName;

NName ist der Name des Namensraums und DName der Name des Datums bzw. der Funktion. Die using-Anweisung erlaubt nur die Angabe eines Namens. Sollen mehrere Namen aus einem Namensraum eingeblendet werden, sind mehrere using-Anweisungen hierfür zu verwenden.

1: // Für Func standardmäßig cppkurs-Namensraum setzen
2: using cppkurs::Func;
3: // Standardmäßig var aus cppkurs Namensraum
4: using cppkurs::var;
5: // Zugriff auf var im Namensraum cppkurs
6: var = 10;
7: // Zugriff auf globales var (in lib.h)
8: ::var = -10;

Und die dritte Möglichkeit ist, alle Namen eines Namensraums einzublenden. Dazu wird folgende using-Direktive verwendet:

using namespace NName;

NName ist wiederum der Name des einzublendenden Namensraums. Diese Anweisung sollte mit Vorsicht angewandt werden, da es dadurch leicht zu Doppeldeutigkeiten von Namen kommen kann.

Geschachtelte Namensräume

Namensräume können geschachtelt werden. Beim Zugriff auf einen Namen sind dann beide Namensräume (von außen nach innen) anzugeben.

1: namespace outer
2: {
3:    namespace inner
4:    {
5:       int var;
6:    }
7: }
8: // Zugriff auf var
9: outer::inner::var = 10;

Alternativ kann der geschachtelte Namensraum außerhalb des umschließenden Namensraums definiert werden. Dazu ist bei der Definition des inneren Namensraums der äußere Namensraum mit anzugeben.

1: namespace outer
2: {
3:    ...
4: }
5: // Innerer Namensraum vom outer
6: namespace outer::inner
7: {
8:    int var;
9: }
10:
11: // Zugriff auf var
12: outer::inner::var = 10;

Anonymer Namensraum

Mithilfe des anonymen Namensraums können Variablen, Funktionen usw. einer Übersetzungseinheit (in der Regel ist dies eine Quelldatei) nach außen hin verborgen werden. Innerhalb der Übersetzungseinheit kann weiterhin auf alle Daten und Funktionen des Namensraums wie gewohnt zugegriffen werden, aber außerhalb der Übersetzungseinheit besteht kein Zugriff darauf. So verhält sich im Beispiel die Variable var innerhalb der Quelldatei wie eine globale Variable, ist aber außerhalb der Übersetzungseinheit nicht sichtbar. Das gleiche Verhalten könnte ebenfalls durch die Definition der Variable var als static Variable erreicht werden.

1: namespace
2: {
3:    int var;
4:    ...
5:    void Func1(...)
6:    {
7:       var = 10;
8:       ...
9:    }
10:   ...
11: }

Inline-Namensraum

Ein inline-Namensraum ist ein Namensraum, der innerhalb eines anderen Namensraums definiert ist. Er wird dadurch gekennzeichnet, dass vor dem Schlüsselwort namespace das Schlüsselwort inline steht.

1: namespace myNamespace
2: {
3:    inline namespace newVersion
4:    {
5:       ...
6:    }
7: }

Auf alle Namen in einem inline-Namensraum kann ohne Angabe des Namens des inline-Namensraums zugegriffen werden.

Wofür ein solcher inline-Namensraum eingesetzt werden kann, soll folgendes Beispiel demonstrieren. Angenommen im Namensraum myLibs ist eine Funktion PrintValue() definiert.

1: // Umgebender Namensraum
2: namespace myLibs
3: {
4:    void PrintValue(int x)
5:    {
6:       std::cout << "Std: " << x << '\n';
7:    }
8: }
9:
10: int main()
11: {
12:    MyLibs::PrintValue(5);
13: }

Diese Funktion soll durch eine neue, optimierte Version ersetzt werden. Aus Kompatibilitätsgründen soll jedoch die ursprüngliche Funktion weiterhin erhalten bleiben und vom Anwender bei Bedarf explizit aufgerufen werden können. Das Problem, das sich hier ergibt, ist, dass die alte und die neue Funktion die gleiche Signatur (Funktionsname und -parameter) haben und sie deswegen nicht im gleichen Namensraum liegen dürfen. Um dieses Problem zu lösen, werden zunächst zwei neue Namensräume eingefügt. Ein Namensraum enthält die alte Funktion und der andere Namensraum die neue Funktion. Damit standardmäßig die neue Funktion aufgerufen wird, wird der Namensraum der neuen Funktion als inline-Namensraum definiert.

1: // Umgebender Namensraum
2: namespace myLibs
3: {
4:     // Version 1 der PrintValue Funktion
5:     namespace Version1
6:     {
7:        void PrintValue(int x)
8:        {
9:           std::cout << "Std: " << x << '\n';
10:       }
11:    }
12:    // inline Namensraum: neue Version 2 der Funktion
13:    inline namespace Version2
14:    {
15:       void PrintValue(int x)
16:       {
17:          std::println("New :{}",x);
18:       }
19:    }
20: }
21:
22: int main()
23: {
24:    // Funktion Version2 aufrufen
25:    myLibs::PrintValue(5);
26:    // Funktion Version1 aufrufen
27:    myLibs::Version1::PrintValue(10);
28: }

Soll standardmäßig die alte Funktion aufgerufen werden, ist lediglich der Namensraum Version1 anstelle des Namensraums Version2 als inline-Namensraum zu definieren.

Da ein inline-Namensraum ein geschachtelter Namensraum ist, kann er auch außerhalb des umschließenden Namensraums definiert werden.

1: // Umgebender Namensraum
2: namespace myLibs
3: {
4:     // Version 1 der PrintValue Funktion
5:     namespace Version1
6:     {
7:        void PrintValue(int x)
8:        {
9:           std::cout << "Std: " << x << '\n';
10:       }
11:    }
12: }
13:
14: // inline Namensraum: neue Version 2 der Funktion
15: namespace myLibs::inline Version2
16: {
17:    void PrintValue(int x)
18:    {
19:       std::println("New :{}",x);
20:    }
21: }

Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz