Blog 5 Min. Lesezeit

7 häufige Gründe für den Einsatz von Namespaces in einem C++-Projekt

Diesen Artikel teilen
7 Common Reasons for Using Namespaces in a C++ Project.

Namespaces wurden 1995 in den C++-Standard aufgenommen und werden üblicherweise wie folgt definiert:

Ein Namespace definiert einen neuen Gültigkeitsbereich. Namespaces bieten eine Möglichkeit, Namenskollisionen zu vermeiden.

Obwohl Namespaces in modernem C++-Code intensiv genutzt werden, kommt dieses Sprachmittel in älterem Code häufig nicht zum Einsatz. 

Nach der Untersuchung des Quellcodes zahlreicher C++-Projekte lassen sich einige häufige Gründe für den Einsatz von Namespaces erkennen.

1- Avoid name collisions.

In diesem Fall kommt ihr Einsatz in erster Linie dem Compiler zugute; für Entwickler entsteht hinsichtlich Lesbarkeit oder Wartbarkeit des Codes kein zusätzlicher Nutzen.

2- Modularize the code

Moderne C++-Bibliotheken nutzen Namespaces intensiv, um ihre Codebasis zu modularisieren. Häufig kommt dabei der Ansatz „Namespace-by-Feature“ zum Einsatz. Dabei spiegeln Namespaces die jeweiligen Funktionen wider: Alle Elemente, die zu einer bestimmten Funktion gehören – und nur diese – werden in einem gemeinsamen Namespace zusammengefasst. Das führt zu Namespaces mit hoher Kohäsion und Modularität sowie geringer Kopplung untereinander. Elemente, die eng zusammenarbeiten, liegen dadurch auch logisch beieinander.

Boost ist ein hervorragendes Beispiel für die Gruppierung nach Funktionen: Die Bibliothek enthält Tausende Namespaces, von denen jeder eine bestimmte Funktion zusammenfasst.

3 – Anonymer Namespace

Ein unbenannter Namespace macht globale statische Variablen überflüssig. Der erzeugte anonyme Namespace ist ausschließlich innerhalb der Datei zugänglich, in der er definiert wurde.

4 – Workaround für das Enum-Problem

„Traditionelle“ Enums in C++ exportieren ihre Enumeratoren in den umgebenden Gültigkeitsbereich. Dadurch können Namenskollisionen entstehen, wenn zwei unterschiedliche Enums im selben Scope Enumeratoren mit identischem Namen definieren.

In einem großen Projekt lässt sich nicht garantieren, dass zwei unterschiedliche Enums keine identischen Namen verwenden. Dieses Problem wurde in C++11 mit enum class, which implicitly scopes the enum values within the enum’s name.

Vor C++11 bestand ein verbreiteter Workaround darin, ein Enum innerhalb eines Namespace zu deklarieren. Statt ein Enum beispielsweise so zu deklarieren:

enum status{
  status_ok,
  status_error
};

konnte es innerhalb eines Namespace deklariert werden:

namespace status{
       enum status{
         ok,
          error
  };
}

Viele C++-Projekte verwenden diesen Trick; im Quellcode der Unreal Engine wird diese Technik beispielsweise häufig eingesetzt.

5 – Implementierungsdetails per Konvention verbergen

Bei Template-Bibliotheken, deren Code in Header-Dateien implementiert ist, ist es sinnvoll, Nutzern zu signalisieren, dass bestimmte Typen nicht direkt verwendet werden sollten, weil sie Implementierungsdetails darstellen. In C# erfüllt das Schlüsselwort „internal“ diesen Zweck; in C++ gibt es dagegen keine Möglichkeit, öffentliche Typen vor Bibliotheksnutzern zu verbergen.

Ein verbreitetes Idiom im modernen C++, das von den Entwicklern der Boost-Bibliotheken geprägt wurde, besteht darin, Symbole, die zur Implementierung eines Moduls gehören – also nicht Teil der öffentlichen API sind –, aber dennoch öffentlich sichtbar sein müssen, in einen separaten Unter-Namespace auszulagern, der konventionsgemäß detail heißt.

In der Boost.Math-Dokumentation heißt es beispielsweise:

Funktionen, die nicht für die Verwendung durch Anwendungen vorgesehen sind, befinden sich in boost::math::detail.

Die mit C++11 eingeführten Inline-Namespaces bieten zwei weitere Möglichkeiten:

Wenn ein Inline-Namespace definiert wird, wird implizit eine using Direktive in den umgebenden Namespace eingefügt. Bei der Suche nach einem qualifizierten Namen über den umgebenden Namespace werden Mitglieder des Inline-Namespace durch die implizite using Direktive einbezogen und gefunden, selbst wenn dieser Name im umgebenden Namespace deklariert ist.

Wenn Sie den folgenden Code beispielsweise mit definiertem USE_INLINE_B kompilieren, gibt das resultierende Programm 1 aus; andernfalls 2.

namespace A {
#if USE_INLINE_B
   inline
#endif
   namespace B {
      int foo(bool) { return 1; }
   }
   int foo(int) { return 2; }
}

int main(void) {
   return A::foo(true);
}

Hier sind zwei weitere Einsatzmöglichkeiten für Inline-Namespaces, wie in diesem Dokument.

6 – Inline-Namespace-Definitionen bei expliziter Instanziierung und Spezialisierung verwenden

Jedes Mitglied eines Inline-Namespace kann explizit instanziiert oder spezialisiert werden, als wäre es ein Mitglied des umgebenden Namespace. Bei der Namenssuche nach dem primären Template einer expliziten Instanziierung oder Spezialisierung in einem Namespace – beispielsweise M – werden Inline-Namespaces berücksichtigt, deren Menge umgebender Namespaces M enthält. Beispiel:

namespace L {
   inline namespace M {
       template <typename T> class C; 
   }

   template <typename T> void f(T) { /*...*/ };
}

struct X { /*...*/ };
namespace L {
  template<> class C<X> { /*...*/ };    //template specialization
}

int main()
{
  L::C<X> r;
  f(r);  // fine, L is an associated namespace of C
}

In diesem Beispiel ist 

M

 ein Inline-Namespace seines umgebenden Namespace 

L

, class 

C

 ist ein Mitglied des Inline-Namespace 

M

, so 

L

 ein zugeordneter Namespace der Klasse 

C

.

Bei der Verwendung von Inline-Namespace-Definitionen für explizite Instanziierung und Spezialisierung gelten folgende Regeln:

  • Eine explizite Instanziierung muss sich in einem umgebenden Namespace des primären Templates befinden, wenn der Template-Name qualifiziert ist. Andernfalls muss sie im nächstgelegenen umgebenden Namespace des primären Templates oder in einem Namespace aus dessen Menge umgebender Namespaces liegen.
  • Eine explizite Spezialisierungsdeklaration muss zunächst im Namespace-Scope des nächstgelegenen umgebenden Namespace des primären Templates oder in einem Namespace aus dessen Menge umgebender Namespaces deklariert werden. Ist die Deklaration keine Definition, kann sie später in jedem umgebenden Namespace definiert werden.

7 – Inline-Namespace-Definitionen zur Versionierung von Bibliotheken verwenden

Mit Inline-Namespace-Definitionen können Sie für eine Bibliothek mit mehreren Implementierungen eine gemeinsame Quellcode-Schnittstelle bereitstellen. Nutzer der Bibliothek können auswählen, welche Implementierung mit dieser gemeinsamen Schnittstelle verknüpft werden soll. Das folgende Beispiel zeigt die Verwendung eines Inline-Namespace bei der Bibliotheksversionierung mit expliziter Spezialisierung.

//foo.h
#ifndef SOME_LIBRARY_FOO_H_
   #define SOME_LIBRARY_FOO_H_
   namespace SomeLibrary
   {
      #ifdef SOME_LIBRARY_USE_VERSION_2_
         inline namespace version_2 { }
      #else    
      	  inline namespace version_1 { }
      #endif
      namespace version_1 {
       	 template <typename T> int foo(T a) {return 1;}
      }     
      namespace version_2 {
       	 template <typename T> int foo(T a) {return 2;}
      }
   }  
#endif 

//myFooCaller.C 
#include <Foo.h>
#include <iostream>

struct MyIntWrapper { int x;}; 

//Specialize SomeLibrary::foo()
//Should specialize the correct version of foo()

namespace SomeLibrary {
	  template <> int foo(MyIntWrapper a) { return a.x;}
}

int main(void) {
	  using namespace SomeLibrary;
	  MyIntWrapper intWrap = { 4 };
	  std::cout << foo(intWrap) + foo(1.0) << std::endl;
}

Wenn Sie dieses Beispiel mit definiertem SOME_LIBRARY_USE_VERSION_2_ kompilieren, gibt das resultierende Programm 6 aus; andernfalls 5. Wenn der Funktionsaufruf 

foo(intWrap)

, mit einem der Inline-Namespaces qualifiziert ist, müssen Sie sicherstellen, dass die explizite Spezialisierung wirksam ist.

Diesen Artikel teilen