434 Stimmen

Wofür sind Inline-Namespaces gut?

C++11 erlaubt inline namespaces, deren alle Elemente auch automatisch im umgebenden namespace sind. Ich kann mir keinen nützlichen Verwendungszweck dafür vorstellen - kann mir bitte jemand ein kurzes, prägnantes Beispiel für eine Situation geben, in der ein inline namespace benötigt wird und wo es die idiomatischste Lösung ist?

(Außerdem ist für mich nicht klar, was passiert, wenn ein namespace in einer, aber nicht allen Deklarationen als inline deklariert wird, die sich möglicherweise in verschiedenen Dateien befinden. Ist das nicht nach Schwierigkeiten?)

415voto

Marc Mutz - mmutz Punkte 23597

Inline-Namensräume sind eine Funktionalität zur Versionsverwaltung von Bibliotheken, ähnlich wie Symbolsversionsverwaltung, aber rein auf der Ebene von C++11 implementiert (d.h. plattformübergreifend) anstelle einer Funktionalität eines spezifischen binären Ausführungsformats (d.h. plattformspezifisch).

Es handelt sich um einen Mechanismus, durch den ein Bibliotheksautor einen verschachtelten Namensraum so aussehen und funktionieren lassen kann, als ob alle seine Deklarationen im umgebenden Namensraum wären (Inline-Namensräume können verschachtelt werden, sodass "weiter geschachtelte" Namen bis zum ersten nicht-inline Namensraum aufsteigen und so aussehen und funktionieren, als ob ihre Deklarationen auch in einem der dazwischenliegenden Namensräume wären).

Als Beispiel betrachten wir die STL-Implementierung von vector. Wenn es von Anfang an Inline-Namensräume in C++ gegeben hätte, dann hätte der Header in C++98 vielleicht so ausgesehen:

namespace std {

#if __cplusplus < 1997L // Vorstandard-C++
    inline
#endif

    namespace pre_cxx_1997 {
        template  __vector_impl; // Implementierungsklasse
        template  // z.B. ohne Allocator-Argument
        class vector : __vector_impl { // private Vererbung
            // ...
        };
    }
#if __cplusplus >= 1997L // C++98/03 oder später
                         // (ifdef'ed out weil es wahrscheinlich neue Sprach
                         // features verwendet, auf die ein Compiler vor C++98 nicht klarkommen würde)
#  if __cplusplus == 1997L // C++98/03
    inline
#  endif

    namespace cxx_1997 {

        // std::vector hat nun ein Allocator-Argument
        template >
        class vector : pre_cxx_1997::__vector_impl { // die alte Implementierung ist noch gut
            // ...
        };

        // und vector ist speziell:
        template >
        class vector {
            // ...
        };

    };

#endif // C++98/03 oder später

} // Namensraum std

Abhängig vom Wert von __cplusplus wird entweder die eine oder die andere Implementierung von vector ausgewählt. Wenn Ihre Codebasis in Zeiten vor C++98 geschrieben wurde und Sie feststellen, dass die C++98-Version von vector Probleme verursacht, wenn Sie Ihren Compiler aktualisieren, müssen Sie "nur" die Referenzen auf std::vector in Ihrer Codebasis finden und durch std::pre_cxx_1997::vector ersetzen.

Kommt der nächste Standard und der STL-Anbieter wiederholt das Verfahren, indem er einen neuen Namensraum für std::vector mit emplace_back-Unterstützung einführt (die C++11 erfordert) und diesen einbettet, falls __cplusplus == 201103L.

Ok, warum brauche ich eine neue Sprachfunktion dafür? Ich kann bereits das Folgende tun, um den gleichen Effekt zu erzielen, oder?

namespace std {

    namespace pre_cxx_1997 {
        // ...
    }
#if __cplusplus < 1997L // Vorstandard-C++
    using namespace pre_cxx_1997;
#endif

#if __cplusplus >= 1997L // C++98/03 oder später
                         // (ifdef'ed out weil es wahrscheinlich neue Sprach
                         // features verwendet, auf die ein Compiler vor C++98 nicht klarkommen würde)

    namespace cxx_1997 {
        // ...
    };
#  if __cplusplus == 1997L // C++98/03
    using namespace cxx_1997;
#  endif

#endif // C++98/03 oder später

} // Namensraum std

Je nach Wert von __cplusplus erhalte ich entweder eine oder die andere der Implementierungen.

Und Sie wären damit fast richtig.

Betrachten Sie den folgenden gültigen C++98-Benutzercode (es war bereits in C++98 erlaubt, Vorlagen vollständig zu spezialisieren, die sich im Namensraum Std befinden):

// Ich traue meinem STL-Anbieter nicht zu, diese Optimierung durchzuführen, also erzwinge
// diese Spezialisierungen selbst:
namespace std {
    template <>
    class vector : my_special_vector {
        // ...
    };
    template <>
    class vector : my_special_vector {
        // ...
    };
    // ...etc...
} // Namensraum std

Dies ist gültiger Code, bei dem der Benutzer seine eigene Implementierung eines Vektors für einen Satz von Typen bereitstellt, bei dem er anscheinend eine effizientere Implementierung kennt als die, die im (seiner Version von) der STL gefunden wird.

Aber: Beim Spezialisieren einer Vorlage müssen Sie dies im Namensraum tun, in dem sie deklariert wurde. Der Standard besagt, dass vector im Namensraum std deklariert ist, also erwartet der Benutzer zu Recht, den Typ im richtigen Namensraum zu spezialisieren.

Dieser Code funktioniert mit einem nicht versionierten Namensraum std oder mit der Inline-Namensraum-Funktionalität von C++11, aber nicht mit dem Versions-Trick, der using namespace verwendet, da dies das Implementierungsdetail offenlegt, dass der wahre Namensraum, in dem Vector definiert wurde, nicht direkt std war.

Es gibt andere Lücken, durch die der verschachtelte Namensraum erkannt werden kann (siehe Kommentare unten), aber Inline-Namensräume schließen sie alle ab. Und das ist eigentlich alles. Enorm nützlich für die Zukunft, aber soweit ich weiß, schreibt der Standard keine Inline-Namensraum-Namen für seine eigene Standardbibliothek vor (ich würde mich jedoch gerne eines Besseren belehren lassen), sodass dies nur für Bibliotheken von Drittanbietern verwendet werden kann, nicht für den Standard selbst (es sei denn, die Compiler-Anbieter stimmen einem Namensschema zu).

31 Stimmen

+1 für die Erklärung, warum using namespace V99; im Beispiel von Stroustrup nicht funktioniert.

2 Stimmen

Ich gehe davon aus, dass die Versionierung in den Standardbibliotheken auf standardisierte Weise im Vergleich zur Versionierung von Drittanbietern begrenzt ist. Wenn C++21 vector für mich nicht geeignet ist und ich C++11 vector benötige, kann das entweder aufgrund einer Umstellungsänderung in C++21 sein. Aber es könnte auch an einem Implementierungsdetail liegen, auf das ich mich nicht hätte verlassen sollen. Der Standard kann nicht verlangen, dass jede C++21-Implementierung eine std::cxx_11::vector bereitstellt, die fehlerkompatibel mit jedem früheren std::vector desselben Anbieters ist. Drittanbieter-Bibliotheken können sich verpflichten, dies zu tun, wenn sie denken, dass es für sie sinnvoll ist.

3 Stimmen

Und ebenso, wenn ich eine brandneue C++21-Implementierung von Grund auf starte, dann möchte ich nicht belastet werden, indem ich viel alten Unsinn in std::cxx_11 implementiere. Nicht jeder Compiler wird immer alle alten Versionen der Standardbibliotheken umsetzen, auch wenn es verlockend ist, im Moment zu denken, dass es sehr wenig Aufwand wäre, bestehende Implementierungen zu verpflichten, das Alte beizubehalten, wenn sie das Neue hinzufügen, da sie tatsächlich sowieso alle sind. Ich nehme an, was der Standard nützlich hätte tun können, ist es optional zu machen, aber mit einem Standardnamen, wenn vorhanden.

91voto

Steve Jessop Punkte 264569

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (ein Dokument, das von Bjarne Stroustrup verfasst und gepflegt wird, der die meisten Beweggründe für die meisten C++11-Features kennen sollte.)

Laut diesem Dokument dient es dazu, Versionierung für Abwärtskompatibilität zu ermöglichen. Sie definieren mehrere innere Namespaces und machen den neuesten inline. Oder sowieso den Standard für Leute, die sich nicht um Versionierung kümmern. Ich vermute, der neueste könnte eine zukünftige oder topaktuelle Version sein, die noch nicht standardmäßig ist.

Das gegebene Beispiel lautet:

// Datei V99.h:
inline namespace V99 {
    void f(int);    // macht etwas besser als die V98-Version
    void f(double); // neue Funktion
    // ...
}

// Datei V98.h:
namespace V98 {
    void f(int);    // macht etwas
    // ...
}

// Datei Mine.h:
namespace Mine {
#include "V99.h"
#include "V98.h"
}

#include "Mine.h"
using namespace Mine;
// ...
V98::f(1);  // alte Version
V99::f(1);  // neue Version
f(1);       // Standardversion

Ich sehe nicht sofort, warum man using namespace V99; nicht innerhalb des Namespace Mine setzen sollte, aber ich muss den Anwendungsfall nicht vollständig verstehen, um Bjarne's Wort für die Motivation des Komitees anzunehmen.

0 Stimmen

Also würde die letzte f(1) Version tatsächlich aus dem Inline-V99 Namespace aufgerufen werden?

1 Stimmen

@EitanT: Ja, weil der globale Namespace using namespace Mine; hat und der Namespace Mine alles aus dem Inline-Namespace Mine::V99 enthält.

0 Stimmen

Um das zu realisieren, müsste ich inline zu Datei V99.h hinzufügen. Was passiert, wenn Datei V100.h auftaucht, die inline namespace V100 deklariert, und ich sie auch in Mine einbinde? Welches f(int) wird von f(1) aufgerufen?

33voto

coder3101 Punkte 3480

Zusätzlich zu allen anderen Antworten.

Inline-Namespace kann verwendet werden, um ABI-Informationen oder Versionen der Funktionen in den Symbolen zu codieren. Aus diesem Grund werden sie verwendet, um eine rückwärtige ABI-Kompatibilität bereitzustellen. Inline-Namespace ermöglichen es Ihnen, Informationen in den Mangled Name (ABI) einzufügen, ohne die API zu ändern, da sie nur den Linker-Symbolnamen beeinflussen.

Betrachten Sie dieses Beispiel:

Angenommen, Sie schreiben eine Funktion Foo, die eine Referenz auf ein Objekt namens bar entgegennimmt und nichts zurückgibt.

Sagen Sie in main.cpp

struct bar;
void Foo(bar& ref);

Wenn Sie den Symbolnamen für diese Datei überprüfen, nachdem Sie sie in ein Objekt kompiliert haben.

$ nm main.o
T__ Z1fooRK6bar

Der Linker-Symbolname kann variieren, aber er wird sicherlich irgendwo den Namen der Funktion und der Argumenttypen codieren.

Es könnte nun sein, dass bar definiert ist als:

struct bar{
   int x;
#ifndef NDEBUG
   int y;
#endif
};

Abhängig vom Build-Typ kann bar auf zwei verschiedene Typen/Strukturen mit denselben Linker-Symbolen verweisen.

Um ein solches Verhalten zu verhindern, verpacken wir unsere Struktur bar in einen Inline-Namespace, in dem je nach Build-Typ das Linker-Symbol von bar unterschiedlich sein wird.

Also könnten wir schreiben:

#ifndef NDEBUG
inline namespace rel { 
#else
inline namespace dbg {
#endif
struct bar{
   int x;
#ifndef NDEBUG
   int y;
#endif
};
}

Wenn Sie nun die Objektdatei jedes Objekts betrachten, das Sie erstellen, eines mit Release und das andere mit Debug-Flag. Werden Sie feststellen, dass die Linker-Symbole auch den Inline-Namespace-Namen enthalten. In diesem Fall

$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar

Die Linker-Symbolnamen können unterschiedlich sein.

Bemerken Sie die Präsenz von rel und dbg in den Symbolnamen.

Nun, wenn Sie versuchen debug mit Release-Modus zu verknüpfen oder umgekehrt werden Sie einen Linker-Fehler erhalten im Gegensatz zu einem Laufzeitfehler.

1 Stimmen

Ja, das macht Sinn. Das ist also eher für Bibliotheks-Implementierer und ähnliches.

11voto

Lewis Kelsey Punkte 3199

Um die Hauptpunkte zusammenzufassen, waren using namespace v99 und inline namespace nicht dasselbe, Ersteres war ein Workaround, um Bibliotheken zu versionieren, bevor ein dediziertes Schlüsselwort (inline) in C++11 eingeführt wurde, das die Probleme bei der Verwendung von using behob und dabei dieselbe Versionierungsfunktionalität bereitstellte. Die Verwendung von using namespace verursachte früher Probleme mit ADL (obwohl ADL jetzt using-Direktiven zu folgen scheint) und die Auslagerung der Spezialisierung einer Bibliotheksklasse / -funktion usw. durch den Benutzer außerhalb des eigentlichen Namensraums funktionierte nicht, wenn dies außerhalb des tatsächlichen Namensraums erfolgte (dessen Name der Benutzer nicht wissen sollte und nicht sollte, d.h. der Benutzer müsste B::abi_v2:: anstatt nur B:: für die Spezialisierung verwenden, um sie aufzulösen).

//Bibliothekscode
namespace B { //Name der Bibliothek, den der Benutzer kennt
    namespace A { //ABI-Version, von der der Benutzer nichts weiß
        template class myclass{int a;};
    }
    using namespace A; //Früherer inline-Namespace-Versionstrick
} 

// Benutzercode
namespace B { //Benutzer denkt, die Bibliothek verwendet diesen Namespace
    template<> class myclass {};
}

Dies zeigt eine statische Analysewarnung first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]. Aber wenn Sie den Namespace A inline machen, löst der Compiler die Spezialisierung korrekt auf. Mit den C++11-Erweiterungen verschwindet das Problem jedoch.

Ausgelagerte Definitionen werden bei Verwendung von using nicht aufgelöst; sie müssen in einem verschachtelten/nicht verschachtelten Erweiterungsnamensraumblock deklariert werden (das bedeutet, dass der Benutzer die ABI-Version erneut kennen muss, falls er aus irgendeinem Grund seine eigene Implementierung einer Funktion bereitstellen durfte).

#include 
namespace A {
    namespace B{
        int a;
        int func(int a);
        template class myclass{int a;};
        class C;
        extern int d;
    } 
    using namespace B;
} 
int A::d = 3; //Kein Element namens 'd' im Namespace A
class A::C {int a;}; //Keine Klasse namens 'C' im Namespace 'A' 
template<> class A::myclass {}; // funktioniert; Spezialisierung ist keine Auslagerung einer Deklaration
int A::func(int a){return a;}; //Auslagerung der Definition von 'func' stimmt mit keiner Deklaration im Namespace 'A' überein
namespace A { int func(int a){return a;};} //funktioniert
int main() {
    A::a =1; // funktioniert; keine Auslagerung der Definition
}

Das Problem verschwindet, wenn man B inline macht.

Der andere Zweck von inline-Namensräumen besteht darin, dem Bibliotheksautor ein transparentes Update der Bibliothek zu ermöglichen, 1) ohne den Benutzer zu zwingen, den Code mit dem neuen Namensraumnamen zu überarbeiten, und 2) einen Mangel an Verbose zu verhindern und 3) Abstraktion von API-unwichtigen Details bereitzustellen, während 4) die gleichen Vorteile bei der Verknüpfungsdiagnose und dem Verhalten bereitzustellen, die ein nicht-inline Namensraum bieten würde. Angenommen, Sie verwenden eine Bibliothek:

namespace library {
    inline namespace abi_v1 {
        class foo {
        } 
    }
}

Es ermöglicht dem Benutzer, library::foo aufzurufen, ohne die ABI-Version in der Dokumentation zu kennen oder einzuschließen, was sauberer aussieht. Das Verwenden von library::abiverison129389123::foo würde unsauber aussehen.

Wenn ein Update für foo vorgenommen wird, z.B. durch Hinzufügen eines neuen Elements zur Klasse, wird dies bestehende Programme auf API-Ebene nicht beeinflussen, da sie das Element nicht bereits verwenden, UND die Änderung am inline-Namespace-Namen wird auf API-Ebene nichts ändern, da library::foo weiterhin funktionieren wird.

namespace library {
    inline namespace abi_v2 {
        class foo {
            //neues Element
        } 
    }
}

Jedoch für Programme, die damit verlinken, da der inline-Namensraumname in Symbolsymbole wie ein regulärer Namensraum eingebettet ist, wird die Änderung für den Linker nicht transparent sein. Wenn also die Anwendung nicht neu kompiliert wird, aber mit einer neuen Version der Bibliothek verlinkt ist, wird ein Fehler angezeigt, dass das Symbol abi_v1 nicht gefunden wurde, anstatt tatsächlich zu verlinken und dann aufgrund einer ABI-Inkompatibilität ein mysteriöser Logikfehler zur Laufzeit zu verursachen. Das Hinzufügen eines neuen Elements wird aufgrund der Änderung in der Typdefinition zu ABI-Kompatibilität führen, auch wenn dies das Programm nicht zur Compile-Zeit (API-Ebene) betrifft.

In diesem Szenario:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

    inline namespace abi_v2 {
        class foo {
            //neues Element
        } 
    }
}

Wie bei der Verwendung von 2 nicht-inline Namensräumen ermöglicht es, eine neue Version der Bibliothek zu verlinken, ohne die Anwendung neu kompilieren zu müssen, weil abi_v1 in einem der globalen Symbole eingebettet wird und es die korrekte (alte) Typdefinition verwendet. Das erneute Kompilieren der Anwendung würde jedoch dazu führen, dass die Verweise zu library::abi_v2 aufgelöst werden.

Die Verwendung von using namespace ist weniger funktional als die Verwendung von inline (da ausgelagerte Definitionen nicht aufgelöst werden), bietet jedoch die gleichen 4 Vorteile wie oben beschrieben. Aber die eigentliche Frage ist, warum weiterhin ein Workaround verwendet wird, wenn es jetzt ein dediziertes Schlüsselwort gibt, um dies zu tun. Es ist eine bessere Praxis, weniger verbal (man muss nur 1 Zeile Code statt 2 ändern) und macht die Absicht klar.

6voto

Matthew Punkte 2283

Ich habe tatsächlich eine weitere Verwendung für Inline-Namespaces entdeckt.

Mit Qt erhält man einige zusätzliche, schöne Funktionen, indem man Q_ENUM_NS verwendet, was wiederum erfordert, dass der umgebende Namespace ein Meta-Objekt hat, das mit Q_NAMESPACE deklariert wird. Damit jedoch Q_ENUM_NS funktioniert, muss es ein entsprechendes Q_NAMESPACE in derselben Datei geben¹. Und es darf nur eins geben, sonst treten doppelte Definitionen auf. Das bedeutet effektiv, dass alle Ihrer Aufzählungen im selben Header sein müssen. Pfui.

Oder... Sie können Inline-Namespaces verwenden. Das Verstecken von Aufzählungen in einem inline namespace führt dazu, dass die Meta-Objekte unterschiedliche mangled-Namen haben, während es für Benutzer so aussieht, als ob der zusätzliche Namespace nicht existiert².

Also sind sie nützlich, um Dinge in mehrere Unternamenräume aufzuteilen, die alle so aussehen, als wären sie ein Namespace, falls Sie das aus irgendeinem Grund benötigen. Natürlich ähnelt dies dem Schreiben von using namespace inner im äußeren Namespace, jedoch ohne die DRY-Verletzung, den Namen des inneren Namespace zweimal zu schreiben.


  1. Eigentlich ist es noch schlimmer; es muss in derselben geschweiften Klammer sein.

  2. Es sei denn, Sie versuchen auf das Meta-Objekt zuzugreifen, ohne es vollständig zu qualifizieren, aber das Meta-Objekt wird fast nie direkt verwendet.

1 Stimmen

Können Sie das mit einem Code-Skelett skizzieren? (idealerweise ohne expliziten Verweis auf Qt). Das klingt alles ziemlich kompliziert/unklar.

2 Stimmen

Nicht ... einfach. Der Grund, warum separate Namespaces benötigt werden, hat mit Qt-Implementierungsdetails zu tun. Um ehrlich zu sein, ist es schwer vorstellbar, in welcher Situation außerhalb von Qt die gleichen Anforderungen bestehen würden. Für dieses Qt-spezifische Szenario sind sie jedoch verdammt nützlich! Weitere Informationen finden Sie unter gist.github.com/mwoehlke-kitware/… oder github.com/Kitware/seal-tk/pull/45 als Beispiel.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X