Wofür sind Inline-Namespaces?

334

C ++ 11 erlaubt inline namespaces, deren Mitglieder sich ebenfalls automatisch im Gehäuse befinden namespace. Ich kann mir keine nützliche Anwendung vorstellen - kann jemand bitte ein kurzes, prägnantes Beispiel für eine Situation geben, in der eine inline namespacebenötigt wird und in der es die idiomatischste Lösung ist?

(Außerdem ist mir nicht klar, was passiert, wenn a in einer, aber nicht in allen Deklarationen namespacedeklariert wird inline, die möglicherweise in verschiedenen Dateien gespeichert sind. Bittet dies nicht um Ärger?)

Walter
quelle

Antworten:

339

Inline-Namespaces sind eine Bibliotheksversionierungsfunktion, die der Symbolversionierung ähnelt , jedoch nur auf C ++ 11-Ebene (dh plattformübergreifend) implementiert wird, anstatt eine Funktion eines bestimmten ausführbaren Binärformats (dh plattformspezifisch) zu sein.

Es ist ein Mechanismus, mit dem ein Bibliotheksautor einen verschachtelten Namespace so aussehen lassen kann, als ob alle seine Deklarationen im umgebenden Namespace wären (Inline-Namespaces können verschachtelt werden, sodass "mehr verschachtelte" Namen bis zum ersten Nicht-Namespace versickern -inline-Namespace und sehen aus und tun so, als ob ihre Deklarationen auch in einem der dazwischen liegenden Namespaces wären).

Betrachten Sie als Beispiel die STL-Implementierung von vector. Wenn wir seit Beginn von C ++ Inline-Namespaces <vector>hätten, hätte der Header in C ++ 98 möglicherweise so ausgesehen:

namespace std {

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

    namespace pre_cxx_1997 {
        template <class T> __vector_impl; // implementation class
        template <class T> // e.g. w/o allocator argument
        class vector : __vector_impl<T> { // private inheritance
            // ...
        };
    }
#if __cplusplus >= 1997L // C++98/03 or later
                         // (ifdef'ed out b/c it probably uses new language
                         // features that a pre-C++98 compiler would choke on)
#  if __cplusplus == 1997L // C++98/03
    inline
#  endif

    namespace cxx_1997 {

        // std::vector now has an allocator argument
        template <class T, class Alloc=std::allocator<T> >
        class vector : pre_cxx_1997::__vector_impl<T> { // the old impl is still good
            // ...
        };

        // and vector<bool> is special:
        template <class Alloc=std::allocator<bool> >
        class vector<bool> {
            // ...
        };

    };

#endif // C++98/03 or later

} // namespace std

Abhängig vom Wert von __cpluspluswird entweder die eine oder die andere vectorImplementierung ausgewählt. Wenn Ihre Codebasis 98-mal vor C ++ geschrieben wurde und Sie feststellen, dass die C ++ 98-Version von vectorIhnen beim Upgrade Ihres Compilers Probleme bereitet, müssen Sie nur die Verweise auf std::vectorin finden Ihre Codebasis und ersetzen Sie sie durch std::pre_cxx_1997::vector.

Kommen Sie zum nächsten Standard, und der STL-Anbieter wiederholt den Vorgang einfach erneut, indem er einen neuen Namespace std::vectormit emplace_backUnterstützung (für den C ++ 11 erforderlich ist) einführt und diesen iff einfügt __cplusplus == 201103L.

OK, warum brauche ich dafür eine neue Sprachfunktion? Ich kann schon folgendes tun, um den gleichen Effekt zu erzielen, nein?

namespace std {

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

#if __cplusplus >= 1997L // C++98/03 or later
                         // (ifdef'ed out b/c it probably uses new language
                         // features that a pre-C++98 compiler would choke on)

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

#endif // C++98/03 or later

} // namespace std

Je nach Wert von __cplusplusbekomme ich entweder die eine oder die andere Implementierung.

Und du wärst fast richtig.

Betrachten Sie den folgenden gültigen C ++ 98-Benutzercode (es war zulässig, Vorlagen, die sich bereits stdin C ++ 98 im Namespace befinden, vollständig zu spezialisieren ):

// I don't trust my STL vendor to do this optimisation, so force these 
// specializations myself:
namespace std {
    template <>
    class vector<MyType> : my_special_vector<MyType> {
        // ...
    };
    template <>
    class vector<MyOtherType> : my_special_vector<MyOtherType> {
        // ...
    };
    // ...etc...
} // namespace std

Dies ist ein vollkommen gültiger Code, bei dem der Benutzer seine eigene Implementierung eines Vektors für eine Reihe von Typen bereitstellt, bei denen er anscheinend eine effizientere Implementierung kennt als die, die in (ihrer Kopie) der STL zu finden ist.

Aber : Wenn Sie eine Vorlage spezialisieren, müssen Sie dies in dem Namespace tun, in dem sie deklariert wurde. Der Standard sagt, dass dies vectorim Namespace deklariert ist std, sodass der Benutzer zu Recht erwartet, den Typ zu spezialisieren.

Dieser Code funktioniert mit einem nicht versionierten Namespace stdoder mit der C ++ 11-Inline-Namespace-Funktion, jedoch nicht mit dem verwendeten Versionierungstrick using namespace <nested>, da dadurch das Implementierungsdetail angezeigt wird, dass der wahre Namespace, in dem vectordefiniert wurde, nicht stddirekt war.

Es gibt andere Lücken, anhand derer Sie den verschachtelten Namespace erkennen können (siehe Kommentare unten), aber Inline-Namespaces schließen sie alle. Und das ist alles. Immens nützlich für die Zukunft, aber AFAIK the Standard schreibt keine Inline-Namespace-Namen für seine eigene Standardbibliothek vor (ich würde mich jedoch gerne als falsch erweisen), sodass es nur für Bibliotheken von Drittanbietern verwendet werden kann, nicht der Standard selbst (es sei denn, die Compiler-Anbieter einigen sich auf ein Namensschema).

Marc Mutz - mmutz
quelle
23
+1 für die Erklärung, warum using namespace V99;in Stroustrups Beispiel nicht funktioniert.
Steve Jessop
3
Und wenn ich eine brandneue C ++ 21-Implementierung von Grund auf neu starte, möchte ich nicht belastet werden, wenn ich viel alten Unsinn in implementiere std::cxx_11. Nicht jeder Compiler wird immer alle alten Versionen der Standardbibliotheken implementieren, auch wenn es im Moment verlockend ist zu glauben, dass es sehr wenig belastend wäre, vorhandene Implementierungen beim Hinzufügen der neuen im alten zu belassen, da sie tatsächlich alle sind sind sowieso. Ich nehme an, was der Standard sinnvollerweise hätte tun können, ist optional, aber mit einem Standardnamen, falls vorhanden.
Steve Jessop
46
Das ist noch nicht alles. ADL war auch ein Grund (ADL folgt nicht mit Anweisungen) und auch die Namenssuche. ( using namespace AIn einem Namespace B werden durch Namen in Namespace B Namen in Namespace A ausgeblendet, wenn Sie danach suchen B::name- nicht so bei Inline-Namespaces.)
Johannes Schaub - litb
4
Warum nicht einfach ifdefs für die vollständige Vektorimplementierung verwenden? Alle Implementierungen würden in einem Namespace sein, aber nur eine von ihnen wird nach der Vorverarbeitung definiert
sasha.sochka
6
@ sasha.sochka, da Sie in diesem Fall keine anderen Implementierungen verwenden können. Sie werden vom Präprozessor entfernt. Mit Inline-Namespaces können Sie jede gewünschte Implementierung verwenden, indem Sie einen vollständig qualifizierten Namen (oder ein usingSchlüsselwort) angeben.
Vasily Biryukov
70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (ein Dokument, das von Bjarne Stroustrup geschrieben und verwaltet wird und der Ihrer Meinung nach die meisten Motivationen für die meisten C ++ 11-Funktionen kennen sollte. )

Demnach soll die Versionierung aus Gründen der Abwärtskompatibilität zugelassen werden. Sie definieren mehrere innere Namespaces und erstellen den neuesten inline. Oder sowieso die Standardeinstellung für Leute, denen die Versionierung egal ist. Ich nehme an, die neueste Version könnte eine zukünftige oder hochmoderne Version sein, die noch nicht standardmäßig ist.

Das gegebene Beispiel ist:

// file V99.h:
inline namespace V99 {
    void f(int);    // does something better than the V98 version
    void f(double); // new feature
    // ...
}

// file V98.h:
namespace V98 {
    void f(int);    // does something
    // ...
}

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

#include "Mine.h"
using namespace Mine;
// ...
V98::f(1);  // old version
V99::f(1);  // new version
f(1);       // default version

Ich verstehe nicht sofort, warum Sie keinen using namespace V99;Namespace eingeben Mine, aber ich muss den Anwendungsfall nicht vollständig verstehen, um Bjarnes Wort dafür auf die Motivation des Komitees zu nehmen.

Steve Jessop
quelle
Tatsächlich würde die letzte f(1)Version aus dem Inline- V99Namespace aufgerufen ?
Eitan T
1
@EitanT: Ja, weil der globale Namespace hat using namespace Mine;und der MineNamespace alles aus dem Inline-Namespace enthält Mine::V99.
Steve Jessop
2
@Walter: Sie entfernen inlineaus der Datei V99.hin der Version, die enthält V100.h. Mine.hGleichzeitig können Sie natürlich auch Änderungen vornehmen , um ein zusätzliches Include hinzuzufügen. Mine.hist Teil der Bibliothek, nicht Teil des Client-Codes.
Steve Jessop
5
@walter: Sie installieren nicht V100.h, sie installieren eine Bibliothek namens "Mine". Es gibt 3 Header-Dateien in Version 99 von "Mine" - Mine.h, V98.hund V99.h. Es gibt 4 Header - Dateien in Version 100 von "Mine" - Mine.h, V98.h, V99.hund V100.h. Die Anordnung der Header-Dateien ist ein Implementierungsdetail, das für Benutzer irrelevant ist. Wenn sie ein Kompatibilitätsproblem entdecken, das bedeutet, dass sie einen Mine::V98::fTeil oder den gesamten Code speziell verwenden müssen, können sie Aufrufe Mine::V98::fvon altem Code mit Aufrufen von Mine::fneu geschriebenem Code mischen .
Steve Jessop
2
@Walter Wie in der anderen Antwort erwähnt, müssen Vorlagen auf den Namespace spezialisiert sein, in dem sie deklariert sind, und nicht auf einen Namespace, in dem sie deklariert sind. Obwohl es seltsam aussieht, können Sie sich auf die Art und Weise, wie es dort gemacht wird, auf Vorlagen spezialisieren Mine, anstatt sich auf Mine::V99oder spezialisieren zu müssen Mine::V98.
Justin Time - Wiedereinsetzung Monica
8

Neben all den anderen Antworten.

Inline-Namespace kann verwendet werden, um ABI-Informationen oder die Version der Funktionen in den Symbolen zu codieren. Aus diesem Grund werden sie verwendet, um Abwärts-ABI-Kompatibilität bereitzustellen. Mit Inline-Namespaces können Sie Informationen in den verstümmelten Namen (ABI) einfügen, ohne die API zu ändern, da sie nur den Namen des Linkersymbols betreffen.

Betrachten Sie dieses Beispiel:

Angenommen, Sie schreiben eine Funktion Foo, die einen Verweis auf ein Objekt sagt barund nichts zurückgibt.

Sagen Sie in main.cpp

struct bar;
void Foo(bar& ref);

Wenn Sie Ihren Symbolnamen für diese Datei überprüfen, nachdem Sie sie zu einem Objekt kompiliert haben.

$ nm main.o
T__ Z1fooRK6bar 

Der Name des Linkersymbols kann variieren, aber er wird sicherlich irgendwo den Namen der Funktions- und Argumenttypen codieren.

Nun könnte es sein, dass bardefiniert ist als:

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

barKann sich je nach Build-Typ auf zwei verschiedene Typen / Layouts mit denselben Linkersymbolen beziehen.

Um ein solches Verhalten zu verhindern, verpacken wir unsere Struktur barin einen Inline-Namespace, in dem das Linkersymbol je nach Build-Typ barunterschiedlich ist.

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 sich nun die Objektdatei jedes Objekts ansehen, erstellen Sie eine mit Release und eine mit Debug-Flag. Sie werden feststellen, dass Linker-Symbole auch Inline-Namespace-Namen enthalten. In diesem Fall

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

Die Namen der Linker-Symbole können unterschiedlich sein.

Beachten Sie das Vorhandensein von relund dbgin den Symbolnamen.

Wenn Sie nun versuchen, das Debuggen mit dem Release-Modus zu verknüpfen oder umgekehrt, wird im Gegensatz zum Laufzeitfehler ein Linker-Fehler angezeigt.

coder3101
quelle
1
Ja, das macht Sinn. Dies ist also eher für Bibliotheksimplementierer und dergleichen.
Walter
3

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

Mit Qt erhalten Sie einige zusätzliche, nette Funktionen Q_ENUM_NS, die wiederum erfordern, dass der einschließende Namespace ein Metaobjekt hat, mit dem deklariert wird Q_NAMESPACE. Um Q_ENUM_NSjedoch arbeiten zu können, muss Q_NAMESPACE in derselben Datei ein entsprechender ⁽¹⁾ vorhanden sein. Und es kann nur einen geben, oder Sie erhalten doppelte Definitionsfehler. Dies bedeutet effektiv, dass sich alle Ihre Aufzählungen im selben Header befinden müssen. Yuck.

Oder ... Sie können Inline-Namespaces verwenden. Das Ausblenden von Aufzählungen in a führt dazu,inline namespacedass die Metaobjekte unterschiedliche verstümmelte Namen haben, während für Benutzer der zusätzliche Namespace nicht vorhanden ist⁽²⁾.

Sie sind also nützlich, um Inhalte in mehrere Sub-Namespaces aufzuteilen, die alle wie ein Namespace aussehen , wenn Sie dies aus irgendeinem Grund tun müssen. Dies ähnelt natürlich dem Schreiben using namespace innerim äußeren Namespace, jedoch ohne die DRY- Verletzung, den Namen des inneren Namespace zweimal zu schreiben.


  1. Es ist tatsächlich schlimmer als das; Es muss sich in derselben Zahnspange befinden.

  2. Es sei denn, Sie versuchen, auf das Metaobjekt zuzugreifen, ohne es vollständig zu qualifizieren, aber das Metaobjekt wird kaum jemals direkt verwendet.

Matthew
quelle
Können Sie das mit einem Code-Skelett skizzieren? (idealerweise ohne expliziten Verweis auf Qt). Es klingt alles ziemlich involviert / unklar.
Walter
Nicht einfach. Der Grund, warum separate Namespaces benötigt werden, hängt mit den Details der Qt-Implementierung zusammen. TBH, es ist schwer vorstellbar, dass eine Situation außerhalb von Qt dieselben Anforderungen hat. Für dieses Qt-spezifische Szenario sind sie jedoch verdammt nützlich! Ein Beispiel finden Sie unter gist.github.com/mwoehlke-kitware/… oder github.com/Kitware/seal-tk/pull/45 .
Matthew
0

So die wichtigsten Punkte zusammenfassen, using namespace v99und inline namespacewaren nicht die gleiche, war die ehemalige eine Abhilfe zu Version Bibliotheken vor einem dedizierten Schlüsselwort (Inline) wurde in C ++ eingeführt 11 , die die Probleme der Verwendung von festen using, während der gleichen Versions Funktionalität. Die Verwendung von using namespaceverwendet, um Probleme mit ADL zu verursachen (obwohl ADL nun den usingAnweisungen zu folgen scheint ) und die Offline-Spezialisierung einer Bibliotheksklasse / -funktion usw. durch den Benutzer würde nicht funktionieren, wenn dies außerhalb des wahren Namespace (dessen Name der Benutzer würde und sollte es nicht wissen, dh der Benutzer müsste B :: abi_v2 :: verwenden und nicht nur B ::, damit die Spezialisierung aufgelöst wird.

//library code
namespace B { //library name the user knows
    namespace A { //ABI version the user doesn't know about
        template<class T> class myclass{int a;};
    }
    using namespace A; //pre inline-namespace versioning trick
} 

// user code
namespace B { //user thinks the library uses this namespace
    template<> class myclass<int> {};
}

Dies zeigt eine Warnung zur statischen Analyse an first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]. Wenn Sie jedoch den Namespace A inline machen, löst der Compiler die Spezialisierung korrekt auf. Mit den C ++ 11-Erweiterungen ist das Problem jedoch behoben.

Out-of-Line-Definitionen werden bei Verwendung nicht aufgelöst using. Sie müssen in einem verschachtelten / nicht verschachtelten Erweiterungs-Namespace-Block deklariert werden (was bedeutet, dass der Benutzer die ABI-Version erneut kennen muss, wenn er aus irgendeinem Grund die Möglichkeit hat, eine eigene Implementierung einer Funktion bereitzustellen).

#include <iostream>
namespace A {
    namespace B{
        int a;
        int func(int a);
        template<class T> class myclass{int a;};
        class C;
        extern int d;
    } 
    using namespace B;
} 
int A::d = 3; //No member named 'd' in namespace A
class A::C {int a;}; //no class named 'C' in namespace 'A' 
template<> class A::myclass<int> {}; // works; specialisation is not an out-of-line definition of a declaration
int A::func(int a){return a;}; //out-of-line definition of 'func' does not match any declaration in namespace 'A'
namespace A { int func(int a){return a;};} //works
int main() {
    A::a =1; // works; not an out-of-line definition
}

Das Problem verschwindet, wenn B inline geschaltet wird.

Die anderen Funktionen, die inlineNamespaces bieten, ermöglichen es dem Bibliotheksschreiber, eine transparente Aktualisierung der Bibliothek bereitzustellen. 1) Ohne den Benutzer zu zwingen, den Code mit dem neuen Namespace-Namen umzugestalten. 2) Verhinderung mangelnder Ausführlichkeit und 3) Abstraktion von API-irrelevanten Details. während 4) die gleiche nützliche Linker-Diagnose und das gleiche Verhalten bieten, die die Verwendung eines Nicht-Inline-Namespace bieten würde. Angenommen, Sie verwenden eine Bibliothek:

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

Der Benutzer kann anrufen, library::fooohne die ABI-Version kennen oder in die Dokumentation aufnehmen zu müssen, die sauberer aussieht. Verwenden library::abiverison129389123::foowürde schmutzig aussehen.

Wenn eine Aktualisierung vorgenommen wird foo, dh ein neues Mitglied zur Klasse hinzugefügt wird, wirkt sich dies nicht auf vorhandene Programme auf API-Ebene aus, da sie das Mitglied nicht bereits verwenden UND die Änderung des Inline-Namespace-Namens auf API-Ebene nichts ändert weil library::foowird noch funktionieren.

namespace library {
    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}

Bei Programmen, die damit verknüpft sind, ist die Änderung für den Linker jedoch nicht transparent, da der Inline-Namespace-Name wie ein regulärer Namespace in Symbolnamen zerlegt wird. Wenn die Anwendung nicht neu kompiliert wird, sondern mit einer neuen Version der Bibliothek verknüpft ist, wird daher ein Symbol abi_v1angezeigt, bei dem der Fehler nicht gefunden wird, anstatt ihn tatsächlich zu verknüpfen, und verursacht zur Laufzeit aufgrund der ABI-Inkompatibilität einen mysteriösen Logikfehler. Das Hinzufügen eines neuen Mitglieds führt aufgrund der Änderung der Typdefinition zu einer ABI-Kompatibilität, auch wenn dies das Programm zur Kompilierungszeit (API-Ebene) nicht beeinflusst.

In diesem Szenario:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}

Wie bei der Verwendung von 2 Nicht-Inline-Namespaces kann eine neue Version der Bibliothek verknüpft werden, ohne dass die Anwendung neu kompiliert werden muss, da abi_v1sie in einem der globalen Symbole entstellt wird und die richtige (alte) Typdefinition verwendet. Das Neukompilieren der Anwendung würde jedoch dazu führen, dass die Verweise aufgelöst werden library::abi_v2.

Die Verwendung using namespaceist weniger funktional als die Verwendung inline(da sich Definitionen außerhalb der Zeile nicht auflösen lassen), bietet jedoch die gleichen 4 Vorteile wie oben. Die eigentliche Frage ist jedoch, warum weiterhin eine Problemumgehung verwendet wird, wenn jetzt ein dediziertes Schlüsselwort dafür vorhanden ist. Es ist besser zu üben, weniger ausführlich (muss 1 Codezeile anstelle von 2 ändern) und macht die Absicht klar.

Lewis Kelsey
quelle