Namensräume

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

Sehen wir uns den Einsatz eines Namensraum an einem (sehr gekürzten) Beispiel an.

Vorgegeben sei das nachfolgende Programm:

// Datei app.cpp

#include <print>
#include <string>
#include "bibl.h"

// Globales Datum zur Farbsteuerung
unsigned long rgbColor;

// Fkt-Definition
void PrintText(int xp, int yp, const std::string& text)
{
    std::println("({},{}):{}",xp,yp,text);
}

int main()
{
    std::string text {"Hallo"};
    rgbColor = 0x808080;
    PrintText(10,20,text);
}

(10,20):Hallo

Da im Verlaufe der Programmentwicklung noch einige zusätzliche Ausgabefunktionen benötigt werden, kommt zusätzlich noch eine Grafikbibliothek zu Einsatz, die auszugsweise folgende beiden Datein enthält:

// Datei bibl.h

extern void PrintText(int xp, int yp, std::string_view text);
// Datei bibl.cpp
#include <print>

// Farbsteuerung fuer Bibliothek
unsigned short rgbColor=0xfff;

void PrintText(int xp, int yp, std::string_view text)
{
    std::println("XPos:{}, YPos:{}, Text: {}",xp,yp,text);
}

Damit diese Grafikbibliothek auch verwendet werden kann, muss natürlich in app.cpp noch die Header-Datei bibl.h mittels #include "bibl.h" eingebunden werden.

Würde die Anwendung nun übersetzt, würde sich der Linker beschweren, dass rgbColor nun mehrfach definiert ist. Und zum Auflösen solcher mehrfachen Definition 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 im Namensraum stehenden Definitionen und Deklarationen sind auch nur innerhalb dieses Namensraums gültig! Gleichnamige Definitionen und Deklarationen in unterschiedlichen Namensräumen erzeugen somit keinen Namenskonflikt mehr.

Wird ein bestehender Namensraum erneut definiert, wird der Namensraum um die neuen Definitionen und Deklarationen erweitert.

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 rgbColor und PrintText nun in verschiedenen Namensräumen liegen, kommt es zu keinen Namenskonflikten mehr.

// Datei app.cpp

#include <print>
#include <string>
#include "bibl.h"

namespace app
{
    // Globales Datum zur Farbsteuerung
    unsigned long rgbColor;

    // Fkt-Definition
    void PrintText(int xp, int yp, const std::string& text)
    {
        std::println("({},{}):{}",xp,yp,text);
    }
}

int main()
{
    std::string text {"Hallo"};
    rgbColor = 0x808080;
    PrintText(10,20,text);
}

Damit ist die mehrfache Definition von rgbColor zwar beseitigt, doch übersetzen lässt sich die Anwendung immer noch nicht. Woher soll der Compiler nun wissen, welches rgbColor in main() gemeint ist?

Zugriff auf Namensräume

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


... Definitionen wie vorher

int main()
{
    std::string text {"Hallo"};
    app::rgbColor = 0x808080;
    app::PrintText(10,20,text);
}

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.


... Definition wie vorher

int main()
{
    using app::rgbColor;
    std::string text {"Hallo"};
    rgbColor = 0x808080;
    PrintText(10,20,text);
}

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.

// Datei app.cpp

#include <print>
#include <string>
#include "bibl.h"

namespace app
{
    namespace graphic
    {
        // Globales Datum zur Farbsteuerung
        unsigned long rgbColor;

        // Fkt-Definition
        void PrintText(int xp, int yp, const std::string& text)
        {
            std::println("({},{}):{}",xp,yp,text);
        }
    }
}

int main()
{
    std::string text {"Hallo"};
    app::graphic::rgbColor = 0x808080;
    app::graphic::PrintText(10,20,text);
}

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.

namespace app
{
    // ...weiter Definition/Deklarateion
}

namespace app::graphic
{
    // Globales Datum zur Farbsteuerung
    unsigned long rgbColor;

    // Fkt-Definition
    void PrintText(int xp, int yp, const std::string& text)
    {
        std::println("({},{}):{}",xp,yp,text);
    }
}

Anonymer Namensraum

Ein anonymer Namensraum ist ein Namensraum ohne Angabe eines Namens. 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.

#include <print>

// Anonymer Namensraum
namespace
{
    int anyData;
    void PrintData();   // Fkt-Deklaration
}

int main()
{
    anyData = 10;
    PrintData();
}

// Die Fkt-Definition muss ebenfalls
// im anonymen Namensraum erfolgten
namespace
{
    void PrintData()
    {
        std::println("Wert: {}",anyData);
    }
}

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.

namespace app
{
   inline namespace newVersion
   {
      // ... Definitionen/Deklarationen
   }
}

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.

#include <iostream>

// Umgebender Namensraum
namespace myLibs
{
    void PrintValue(int x)
    {
        std::cout << "cout: " << x << '\n';
    }
}
int main()
{
    myLibs::PrintValue(5);
}

cout: 5

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 innerhalb des bestehenden Namensraums 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.

#include <iostream>
#include <print>
// Umgebender Namensraum
namespace myLibs
{
    // Version 1 der PrintValue Funktion
    namespace Version1
    {
        void PrintValue(int x)
        {
            std::cout << "cout: " << x << '\n';
        }
    } // Ende Version1
    // inline Namensraum: neue Version 2 der Funktion
    inline namespace Version2
    {
        void PrintValue(int x)
        {
            std::println("println :{}",x);
        }
    } // Ende Version2
} // Ende myLibs

int main()
{
    // Funktion Version2 aufrufen
    myLibs::PrintValue(5);
    // Funktion Version1 aufrufen
    myLibs::Version1::PrintValue(10);
}

println :5
cout: 10

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 (wie weiter oben angegeben).

Angemerkt werden soll an dieser Stelle, dass für einen Namensraum Attribute angegeben werden können. Mehr zu den Attributen im Kapitel Präprozessor-Direktiven und Attribute.