A cosa servono gli spazi dei nomi in linea?


334

C ++ 11 consente a inline namespaces, tutti i membri dei quali sono automaticamente inclusi nell'allegato namespace. Non riesco a pensare a nessuna utile applicazione di questo - qualcuno può per favore dare un breve, breve esempio di una situazione in cui inline namespaceè necessario e dove è la soluzione più idiomatica?

(Inoltre, non mi è chiaro cosa succede quando a namespace viene dichiarato inlinein una, ma non tutte le dichiarazioni, che possono risiedere in file diversi. Non è questo chiedere l'elemosina?)

Risposte:


339

Gli spazi dei nomi in linea sono una funzione di versioning della libreria simile alla versione dei simboli , ma implementata esclusivamente a livello di C ++ 11 (cioè multipiattaforma) invece di essere una funzionalità di un formato eseguibile binario specifico (cioè specifico della piattaforma).

È un meccanismo attraverso il quale un autore di libreria può far apparire e agire uno spazio dei nomi nidificato come se tutte le sue dichiarazioni fossero nello spazio dei nomi circostante (gli spazi dei nomi in linea possono essere nidificati, quindi i nomi "più nidificati" percorrono fino al primo non -inline lo spazio dei nomi e guarda e agisci come se anche le loro dichiarazioni fossero in uno degli spazi dei nomi in mezzo).

Ad esempio, prendere in considerazione l'implementazione STL di vector. Se avessimo spazi dei nomi in linea dall'inizio di C ++, allora in C ++ 98 l'intestazione <vector>avrebbe potuto apparire così:

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

A seconda del valore di __cplusplus, viene scelta l'una o l'altra vectorimplementazione. Se la tua base di codice è stata scritta in pre-C ++ 98 volte e scopri che la versione di C ++ 98 ti vectorsta causando problemi quando aggiorni il compilatore, "tutto" non devi fare altro che trovare i riferimenti a std::vectorin il tuo codebase e sostituiscili con std::pre_cxx_1997::vector.

Arriva lo standard successivo e il fornitore STL ripete nuovamente la procedura, introducendo un nuovo spazio dei nomi std::vectorcon emplace_backsupporto (che richiede C ++ 11) e incorporando quello iff __cplusplus == 201103L.

OK, quindi perché ho bisogno di una nuova funzionalità linguistica per questo? Posso già fare quanto segue per avere lo stesso effetto, no?

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

A seconda del valore di __cplusplus, ottengo una o l'altra delle implementazioni.

E avresti quasi ragione.

Si consideri il seguente codice utente C ++ 98 valido (era permesso specializzare completamente i modelli che vivono già nello spazio dei nomi stdin C ++ 98):

// 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

Questo è un codice perfettamente valido in cui l'utente fornisce la propria implementazione di un vettore per un set di tipi in cui apparentemente conosce un'implementazione più efficiente di quella trovata nella (sua copia) della STL.

Ma : quando si specializza un modello, è necessario farlo nello spazio dei nomi in cui è stato dichiarato. Lo standard dice che vectorè dichiarato nello spazio dei nomistd , quindi è lì che l'utente si aspetta giustamente di specializzare il tipo.

Questo codice funziona con uno spazio dei nomi senza versione stdo con la funzionalità dello spazio dei nomi in linea C ++ 11, ma non con il trucco di versione utilizzato using namespace <nested>, poiché espone i dettagli di implementazione che lo spazio dei nomi vero in cui è vectorstato definito non era stddirettamente.

Esistono altri buchi mediante i quali è possibile rilevare lo spazio dei nomi nidificato (vedere i commenti seguenti), ma gli spazi dei nomi in linea li collegano tutti. E questo è tutto. Immensamente utile per il futuro, ma AFAIK lo standard non prescrive nomi di spazi dei nomi in linea per la propria libreria standard (mi piacerebbe essere smentito su questo, però), quindi può essere utilizzato solo per librerie di terze parti, non lo standard stesso (a meno che i fornitori del compilatore non concordino uno schema di denominazione).


23
+1 per spiegare perché using namespace V99;non funziona nell'esempio di Stroustrup.
Steve Jessop,

3
Allo stesso modo, se inizio una nuovissima implementazione C ++ 21 da zero, allora non voglio essere gravato dall'implementazione di molte vecchie sciocchezze std::cxx_11. Non tutti i compilatori implementeranno sempre tutte le vecchie versioni delle librerie standard, anche se al momento è allettante pensare che sarebbe poco oneroso richiedere che le implementazioni esistenti lascino nel vecchio quando aggiungono il nuovo, poiché in realtà tutti lo sono comunque. Suppongo che ciò che lo standard avrebbe potuto fare utilmente sia stato reso facoltativo, ma con un nome standard se presente.
Steve Jessop,

46
Non è tutto quello che c'è da fare. Anche l'ADL era un motivo (ADL non seguirà le direttive) e anche la ricerca del nome. ( using namespace Ain uno spazio dei nomi B, i nomi nello spazio dei nomi B nascondono i nomi nello spazio dei nomi A se si cerca B::name- non così con gli spazi dei nomi incorporati).
Johannes Schaub - litb

4
Perché non usare solo ifdefs per l'implementazione vettoriale completa? Tutte le implementazioni si troverebbero in uno spazio dei nomi, ma solo una di esse verrà definita dopo la preelaborazione
sasha.sochka

6
@ sasha.sochka, perché in questo caso non è possibile utilizzare altre implementazioni. Saranno rimossi dal preprocessore. Con gli spazi dei nomi in linea è possibile utilizzare qualsiasi implementazione desiderata specificando il nome completo (o usingparola chiave).
Vasily Biryukov,

70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (un documento scritto e gestito da Bjarne Stroustrup, che secondo te dovrebbe essere a conoscenza della maggior parte delle motivazioni per la maggior parte delle funzionalità di C ++ 11. )

In base a ciò, è per consentire il controllo delle versioni per compatibilità con le versioni precedenti. Definisci più spazi dei nomi interni e ne crei uno più recente inline. O comunque, quello predefinito per le persone a cui non interessa il controllo delle versioni. Suppongo che la più recente potrebbe essere una versione futura o all'avanguardia che non è ancora predefinita.

L'esempio fornito è:

// 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

Non vedo immediatamente perché non metti nello using namespace V99;spazio dei nomi Mine, ma non devo capire del tutto il caso d'uso al fine di prendere la parola di Bjarne sulla motivazione del comitato.


Quindi in effetti l'ultima f(1)versione verrebbe chiamata dallo V99spazio dei nomi in linea ?
Eitan T,

1
@EitanT: sì, perché lo spazio dei nomi globale ha using namespace Mine;, e lo Minespazio dei nomi contiene tutto dallo spazio dei nomi in linea Mine::V99.
Steve Jessop,

2
@Walter: si rimuove inlinedal file V99.hnella versione che include V100.h. Puoi anche modificare Mine.hallo stesso tempo, ovviamente, per aggiungere un'inclusione aggiuntiva. Mine.hfa parte della libreria, non fa parte del codice client.
Steve Jessop,

5
@Walter: non stanno installando V100.h, stanno installando una libreria chiamata "Mine". Ci sono 3 file di intestazione nella versione 99 di "Mine" - Mine.h, V98.he V99.h. Ci sono 4 file di intestazione nella versione 100 di "Mine" - Mine.h, V98.h, V99.he V100.h. La disposizione dei file di intestazione è un dettaglio di implementazione irrilevante per gli utenti. Se scoprono alcuni problemi di compatibilità, il che significa che devono utilizzare in modo specifico Mine::V98::falcuni o tutti i loro codici, possono mescolare le chiamate Mine::V98::fdal vecchio codice con le chiamate Mine::fnel nuovo codice scritto.
Steve Jessop,

2
@Walter Come menzionano le altre risposte, i modelli devono essere specializzati nello spazio dei nomi in cui sono dichiarati, non in uno spazio dei nomi che utilizza quello in cui sono dichiarati. Mentre sembra strano, il modo in cui è fatto lì ti consente di specializzare i modelli in Mine, invece di specializzarsi in Mine::V99o Mine::V98.
Justin Time - Ripristina Monica il

8

Oltre a tutte le altre risposte.

Lo spazio dei nomi incorporato può essere utilizzato per codificare le informazioni ABI o la versione delle funzioni nei simboli. È per questo motivo che vengono utilizzati per fornire compatibilità ABI all'indietro. Gli spazi dei nomi incorporati consentono di inserire informazioni nel nome alterato (ABI) senza alterare l'API perché influiscono solo sul nome del simbolo del linker.

Considera questo esempio:

Supponiamo di scrivere una funzione Fooche accetta un riferimento a un oggetto dire bare non restituisce nulla.

Dì in main.cpp

struct bar;
void Foo(bar& ref);

Se controlli il nome del tuo simbolo per questo file dopo averlo compilato in un oggetto.

$ nm main.o
T__ Z1fooRK6bar 

Il nome del simbolo del linker può variare ma sicuramente codificherà il nome della funzione e dei tipi di argomento da qualche parte.

Ora, potrebbe essere bardefinito come:

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

A seconda del tipo di build, barpuò fare riferimento a due diversi tipi / layout con gli stessi simboli del linker.

Per evitare tale comportamento, avvolgiamo la nostra struttura barin uno spazio dei nomi in linea, in cui a seconda del tipo di build il simbolo del linker barsarà diverso.

Quindi, potremmo scrivere:

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

Ora se guardi il file oggetto di ogni oggetto ne crei uno usando release e l'altro con flag di debug. Scoprirai che i simboli del linker includono anche il nome dello spazio dei nomi in linea. In questo caso

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

I nomi dei simboli del linker potrebbero essere diversi.

Notare la presenza di rele dbgnei nomi dei simboli.

Ora, se provi a collegare il debug con la modalità di rilascio o viceversa, otterrai un errore del linker in contrasto con l'errore di runtime.


1
Sì, ha senso. Quindi questo è più per gli implementatori di librerie e simili.
Walter,

3

In realtà ho scoperto un altro uso per gli spazi dei nomi in linea.

Con Qt , si ottengono alcune funzionalità aggiuntive e piacevoli Q_ENUM_NS, che a loro volta richiedono che lo spazio dei nomi racchiuso abbia un meta oggetto, che viene dichiarato con Q_NAMESPACE. Tuttavia, per Q_ENUM_NSfunzionare, deve esserci un corrispondente Q_NAMESPACE nello stesso file ⁽¹⁾. E può essercene solo uno o si ottengono errori di definizione duplicati. Questo, effettivamente, significa che tutte le tue enumerazioni devono essere nella stessa intestazione. Che schifo.

Oppure ... puoi usare spazi dei nomi in linea. Nascondere le enumerazioni in uninline namespacefa sì che i metaoggetti abbiano nomi alterati diversi, mentre non si guarda agli utenti come lo spazio dei nomi aggiuntivo⁽²⁾.

Quindi, sono utili per dividere le cose in più spazi dei nomi secondari che sembrano tutti uno spazio dei nomi, se è necessario farlo per qualche motivo. Naturalmente, questo è simile alla scrittura using namespace innernello spazio dei nomi esterno, ma senza la violazione DRY di scrivere il nome dello spazio dei nomi interno due volte.


  1. In realtà è peggio di così; deve essere nello stesso set di parentesi graffe.

  2. A meno che non provi ad accedere al meta-oggetto senza qualificarlo completamente, ma il meta-oggetto non viene quasi mai usato direttamente.


Puoi abbozzarlo con uno scheletro di codice? (idealmente senza riferimento esplicito a Qt). Sembra tutto piuttosto coinvolto / poco chiaro.
Walter,

Non ... facilmente. Il motivo per cui sono necessari spazi dei nomi separati ha a che fare con i dettagli dell'implementazione di Qt. TBH, è difficile immaginare una situazione al di fuori di Qt che avrebbe gli stessi requisiti. Tuttavia, per questo scenario specifico di Qt, sono dannatamente utili! Vedere gist.github.com/mwoehlke-kitware/… o github.com/Kitware/seal-tk/pull/45 per un esempio.
Matteo,

0

Quindi, per riassumere i punti principali, using namespace v99e inline namespacenon erano gli stessi, il primo era una soluzione alternativa alle librerie di versioni prima che una parola chiave dedicata (inline) fosse introdotta in C ++ 11 che risolvesse i problemi di utilizzo using, fornendo allo stesso tempo la stessa funzionalità di controllo delle versioni. L'uso using namespaceutilizzato per causare problemi con ADL (sebbene ADL ora sembri seguire le usingdirettive) e la specializzazione fuori linea di una classe / funzione di libreria ecc. Da parte dell'utente non funzionerebbe se eseguita al di fuori del vero spazio dei nomi (il cui nome è il l'utente non dovrebbe e non dovrebbe saperlo, ovvero l'utente dovrebbe usare B :: abi_v2 :: piuttosto che solo B :: per la specializzazione da risolvere).

//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> {};
}

Questo mostrerà un avviso di analisi statica first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]. Ma se rendi lo spazio dei nomi A in linea, il compilatore risolve correttamente la specializzazione. Sebbene, con le estensioni C ++ 11, il problema scompaia.

Le definizioni fuori linea non si risolvono durante l'utilizzo using; devono essere dichiarati in un blocco dello spazio dei nomi di estensione nidificato / non nidificato (il che significa che l'utente deve conoscere nuovamente la versione ABI, se per qualsiasi motivo gli è stato permesso di fornire la propria implementazione di una funzione).

#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
}

Il problema scompare quando si fa B in linea.

Gli altri inlinespazi dei nomi delle funzionalità consentono al writer della libreria di fornire un aggiornamento trasparente alla libreria 1) senza forzare l'utente a refactificare il codice con il nuovo nome dello spazio dei nomi e 2) prevenire la mancanza di verbosità e 3) fornire l'astrazione di dettagli irrilevanti per le API, mentre 4) fornire la stessa diagnostica e comportamento utili del linker che fornirebbe uno spazio dei nomi non in linea. Supponiamo che tu stia utilizzando una libreria:

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

Consente all'utente di chiamare library::foosenza la necessità di conoscere o includere la versione ABI nella documentazione, che sembra più pulita. L'uso library::abiverison129389123::foosembrerebbe sporco.

Quando viene effettuato un aggiornamento foo, vale a dire l'aggiunta di un nuovo membro alla classe, non influirà sui programmi esistenti a livello di API perché non utilizzeranno già il membro E la modifica del nome dello spazio dei nomi in linea non cambierà nulla a livello di API perché library::foofunzionerà ancora.

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

Tuttavia, per i programmi che si collegano ad esso, poiché il nome dello spazio dei nomi in linea viene modificato in nomi di simboli come uno spazio dei nomi normale, la modifica non sarà trasparente al linker. Pertanto, se l'applicazione non viene ricompilata ma è collegata a una nuova versione della libreria, presenterà un simbolo che abi_v1non è stato trovato errore, piuttosto che collegarsi effettivamente e quindi causare un misterioso errore logico in fase di esecuzione a causa dell'incompatibilità ABI. L'aggiunta di un nuovo membro provocherà la compatibilità ABI a causa della modifica della definizione del tipo, anche se non influisce sul programma al momento della compilazione (livello API).

In questo scenario:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

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

Come l'utilizzo di 2 spazi dei nomi non in linea, consente di collegare una nuova versione della libreria senza dover ricompilare l'applicazione, poiché abi_v1verrà modificata in uno dei simboli globali e utilizzerà la definizione del tipo (vecchio) corretta. La ricompilazione dell'applicazione causerebbe tuttavia la risoluzione dei riferimenti library::abi_v2.

L'uso using namespaceè meno funzionale dell'uso inline(in quanto le definizioni fuori linea non si risolvono) ma offre gli stessi 4 vantaggi di cui sopra. Ma la vera domanda è: perché continuare a usare una soluzione alternativa quando ora c'è una parola chiave dedicata per farlo. È una pratica migliore, meno dettagliata (è necessario cambiare 1 riga di codice anziché 2) e chiarire l'intenzione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.