¿Para qué son los espacios de nombres en línea?


334

C ++ 11 permite inline namespaces, todos los miembros de los cuales también están automáticamente en el recinto namespace. No puedo pensar en ninguna aplicación útil de esto: ¿alguien puede dar un breve y sucinto ejemplo de una situación en la que inline namespacese necesita y donde es la solución más idiomática?

(Además, no está claro para mí qué sucede cuando namespacese declara a inlineen una, pero no en todas las declaraciones, que pueden vivir en archivos diferentes. ¿No es esto un problema?)

Respuestas:


339

Los espacios de nombres en línea son una característica de versiones de la biblioteca similar a las versiones de símbolos , pero se implementan exclusivamente en el nivel C ++ 11 (es decir, multiplataforma) en lugar de ser una característica de un formato ejecutable binario específico (es decir, específico de la plataforma).

Es un mecanismo por el cual el autor de una biblioteca puede hacer que un espacio de nombres anidado se vea y actúe como si todas sus declaraciones estuvieran en el espacio de nombres circundante (los espacios de nombres en línea se pueden anidar, por lo que los nombres "más anidados" se filtran hasta el primer no -inline espacio de nombres y se ven y actúan como si sus declaraciones estuvieran en cualquiera de los espacios de nombres intermedios, también).

Como ejemplo, considere la implementación de STL de vector. Si tuviéramos espacios de nombres en línea desde el comienzo de C ++, entonces en C ++ 98 el encabezado <vector>podría tener este aspecto:

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

Dependiendo del valor de __cplusplus, vectorse elige una u otra implementación. Si su código base fue escrito en versiones anteriores a C ++ 98 veces, y encuentra que la versión de C ++ 98 le vectorestá causando problemas cuando actualiza su compilador, "todo" que tiene que hacer es encontrar las referencias std::vectoren su base de código y reemplácelos por std::pre_cxx_1997::vector.

Llegue al siguiente estándar, y el proveedor de STL simplemente repite el procedimiento nuevamente, presentando un nuevo espacio de nombres std::vectorcon emplace_backsoporte (que requiere C ++ 11) e integrando ese iff __cplusplus == 201103L.

Bien, entonces ¿por qué necesito una nueva función de idioma para esto? Ya puedo hacer lo siguiente para tener el mismo efecto, ¿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

Dependiendo del valor de __cplusplus, obtengo una u otra de las implementaciones.

Y estarías casi en lo correcto.

Considere el siguiente código de usuario válido de C ++ 98 (ya estaba permitido especializar completamente las plantillas que viven en el espacio stdde nombres en 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

Este es un código perfectamente válido en el que el usuario proporciona su propia implementación de un vector para un conjunto de tipos en el que aparentemente conoce una implementación más eficiente que la que se encuentra en (su copia de) el STL.

Pero : cuando se especializa una plantilla, debe hacerlo en el espacio de nombres en el que se declaró. El Estándar dice que vectorse declara en el espacio de nombres std, por lo que es allí donde el usuario espera especializar el tipo.

Este código funciona con un espacio de nombres no versionado std, o con la función de espacio de nombres en línea C ++ 11, pero no con el truco de versiones utilizado using namespace <nested>, porque eso expone el detalle de implementación de que el verdadero espacio de nombres en el que vectorse definió no estaba stddirectamente.

Hay otros agujeros por los cuales puede detectar el espacio de nombres anidado (vea los comentarios a continuación), pero los espacios de nombres en línea los conectan a todos. Y eso es todo lo que hay que hacer. Inmensamente útil para el futuro, pero AFAIK the Standard no prescribe nombres de espacios de nombres en línea para su propia biblioteca estándar (sin embargo, me encantaría que se demuestre lo contrario), por lo que solo se puede usar para bibliotecas de terceros, no el estándar en sí (a menos que los proveedores del compilador acuerden un esquema de nombres).


23
+1 para explicar por qué using namespace V99;no funciona en el ejemplo de Stroustrup.
Steve Jessop

3
Y de manera similar, si comienzo una nueva implementación de C ++ 21 desde cero, entonces no quiero tener la carga de implementar muchas tonterías antiguas std::cxx_11. No todos los compiladores siempre implementarán todas las versiones antiguas de las bibliotecas estándar, a pesar de que en este momento es tentador pensar que sería una carga muy pequeña exigir que las implementaciones existentes dejen en la versión anterior cuando agreguen la nueva, ya que de hecho todas son de todos modos Supongo que lo que el estándar podría haber hecho útil es hacerlo opcional, pero con un nombre estándar si está presente.
Steve Jessop

46
Eso no es todo lo que hay que hacer. ADL también fue una razón (ADL no seguirá el uso de directivas), y la búsqueda de nombres también. ( using namespace Aen un espacio de nombres B hace que los nombres en el espacio de nombres B oculten nombres en el espacio de nombres A si lo busca B::name, no es así con espacios de nombres en línea).
Johannes Schaub - litb

44
¿Por qué no solo usar ifdefs para la implementación de vector completo? Todas las implementaciones estarían en un espacio de nombres, pero solo una de ellas se definirá después del preprocesamiento
sasha.sochka

66
@ sasha.sochka, porque en este caso no puedes usar otras implementaciones. Serán eliminados por el preprocesador. Con espacios de nombres en línea, puede usar cualquier implementación que desee especificando un nombre completo (o usingpalabra clave).
Vasily Biryukov

70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (un documento escrito y mantenido por Bjarne Stroustrup, quien crees que debería tener en cuenta la mayoría de las motivaciones para la mayoría de las características de C ++ 11. )

De acuerdo con eso, es permitir el control de versiones para la compatibilidad con versiones anteriores. Define múltiples espacios de nombres internos y crea el más reciente inline. O de todos modos, el predeterminado para las personas que no se preocupan por el versionado. Supongo que la más reciente podría ser una versión futura o de vanguardia que aún no es predeterminada.

El ejemplo dado es:

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

No veo de inmediato por qué no se coloca using namespace V99;dentro del espacio de nombres Mine, pero no tengo que entender completamente el caso de uso para tomar la palabra de Bjarne en la motivación del comité.


¿De hecho, la última f(1)versión se llamaría desde el V99espacio de nombres en línea ?
Eitan T

1
@EitanT: sí, porque el espacio de nombres global tiene using namespace Mine;, y el Mineespacio de nombres contiene todo, desde el espacio de nombres en línea Mine::V99.
Steve Jessop

2
@Walter: elimina inlinedel archivo V99.hen la versión que incluye V100.h. También modifica Mine.hal mismo tiempo, por supuesto, para agregar una inclusión adicional. Mine.hes parte de la biblioteca, no parte del código del cliente.
Steve Jessop

55
@walter: no están instalando V100.h, están instalando una biblioteca llamada "Mine". Hay 3 archivos de encabezado en la versión 99 de "Mine" - Mine.h, V98.hy V99.h. Hay 4 archivos de cabecera en la versión 100 de "Mine" - Mine.h, V98.h, V99.hy V100.h. La disposición de los archivos de encabezado es un detalle de implementación que es irrelevante para los usuarios. Si descubren algún problema de compatibilidad, lo que significa que deben usar específicamente Mine::V98::fparte de todo o parte de su código, pueden mezclar llamadas Mine::V98::fdesde el código antiguo con llamadas al Mine::fcódigo recién escrito.
Steve Jessop

2
@Walter Como se menciona en la otra respuesta, las plantillas deben estar especializadas en el espacio de nombres en el que están declaradas, no en un espacio de nombres en el que están declaradas. Si bien parece extraño, la forma en que se hace allí le permite especializarse en plantillas Mine, en lugar de tener que especializarse en Mine::V99o Mine::V98.
Justin Time - Restablece a Monica

8

Además de todas las otras respuestas.

El espacio de nombres en línea se puede utilizar para codificar información ABI o versión de las funciones en los símbolos. Es por esta razón que se utilizan para proporcionar compatibilidad ABI hacia atrás. Los espacios de nombres en línea le permiten inyectar información en el nombre mutilado (ABI) sin alterar la API porque solo afectan al nombre del símbolo del vinculador.

Considere este ejemplo:

Supongamos que escribe una función Fooque toma una referencia a un objeto bary no devuelve nada.

Decir en main.cpp

struct bar;
void Foo(bar& ref);

Si comprueba el nombre de su símbolo para este archivo después de compilarlo en un objeto.

$ nm main.o
T__ Z1fooRK6bar 

El nombre del símbolo del enlazador puede variar, pero seguramente codificará el nombre de la función y los tipos de argumento en alguna parte.

Ahora, podría ser que barse define como:

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

Dependiendo del tipo de construcción, barpuede referirse a dos tipos / diseños diferentes con los mismos símbolos de enlace.

Para evitar tal comportamiento, envolvemos nuestra estructura baren un espacio de nombres en línea, donde dependiendo del tipo de construcción, el símbolo del enlazador barserá diferente.

Entonces, podríamos escribir:

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

Ahora, si mira el archivo de objeto de cada objeto, construye uno usando la versión y otro con el indicador de depuración. Encontrará que los símbolos del enlazador también incluyen el nombre del espacio de nombres en línea. En este caso

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

Los nombres de los símbolos de enlazador pueden ser diferentes.

Observe la presencia de rely dbgen los nombres de los símbolos.

Ahora, si intenta vincular la depuración con el modo de liberación o viceversa, obtendrá un error de vinculador como contrario al error de tiempo de ejecución.


1
Sí, eso tiene sentido. Esto es más para implementadores de bibliotecas y similares.
Walter

3

De hecho, descubrí otro uso para los espacios de nombres en línea.

Con Qt , obtienes algunas características adicionales y agradables Q_ENUM_NSque, a su vez, requieren que el espacio de nombres incluido tenga un metaobjeto, que se declara con Q_NAMESPACE. Sin embargo, para Q_ENUM_NSque funcione, debe haber un correspondiente Q_NAMESPACE en el mismo archivo ⁽¹⁾. Y solo puede haber uno, u obtienes errores de definición duplicados. Esto, efectivamente, significa que todas sus enumeraciones deben estar en el mismo encabezado. Yuck

O ... puede usar espacios de nombres en línea. Ocultar enumeraciones en uninline namespacehace que los metaobjetos tengan diferentes nombres destrozados, mientras que a los usuarios les parece que el espacio de nombres adicional no existe⁽²⁾.

Por lo tanto, son útiles para dividir cosas en múltiples subespacios de nombres que se parecen a un espacio de nombres, si necesita hacerlo por alguna razón. Por supuesto, esto es similar a escribir using namespace inneren el espacio de nombres externo, pero sin la violación DRY de escribir dos veces el nombre del espacio de nombres interno.


  1. En realidad es peor que eso; tiene que estar en el mismo conjunto de llaves.

  2. A menos que intente acceder al metaobjeto sin calificarlo por completo, pero el metaobjeto casi nunca se usa directamente.


¿Puedes dibujar eso con un esqueleto de código? (idealmente sin referencia explícita a Qt). Todo suena bastante complicado / poco claro.
Walter

No es fácil. La razón por la que se necesitan espacios de nombres separados tiene que ver con los detalles de implementación de Qt. TBH, es difícil imaginar una situación fuera de Qt que tenga los mismos requisitos. Sin embargo, para este escenario específico de Qt, ¡son muy útiles! Consulte gist.github.com/mwoehlke-kitware/… o github.com/Kitware/seal-tk/pull/45 para ver un ejemplo.
Mateo

0

Entonces, para resumir los puntos principales, using namespace v99y inline namespaceno eran lo mismo, el primero era una solución alternativa a las bibliotecas de versiones antes de que se introdujera una palabra clave dedicada (en línea) en C ++ 11 que solucionaba los problemas de uso using, al tiempo que proporcionaba la misma funcionalidad de versiones. El uso using namespaceutilizado para causar problemas con ADL (aunque ahora parece que ADL sigue las usingdirectivas), y la especialización fuera de línea de una clase / función de biblioteca, etc. por parte del usuario no funcionaría si se realiza fuera del espacio de nombres verdadero (cuyo nombre el usuario no debería ni debería saberlo, es decir, el usuario tendría que usar B :: abi_v2 :: en lugar de solo B :: para que se resolviera la especialización).

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

Esto mostrará una advertencia de análisis estático first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]. Pero si hace que el espacio de nombres A esté en línea, el compilador resuelve correctamente la especialización. Aunque, con las extensiones de C ++ 11, el problema desaparece.

Las definiciones fuera de línea no se resuelven cuando se usan using; deben declararse en un bloque de espacio de nombres de extensión anidado / no anidado (lo que significa que el usuario necesita conocer la versión ABI nuevamente, si por alguna razón se les permitió proporcionar su propia implementación de una función).

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

El problema desaparece cuando se hace B en línea.

Los otros inlineespacios de nombres funcionales permiten al escritor de la biblioteca proporcionar una actualización transparente de la biblioteca 1) sin obligar al usuario a refactorizar el código con el nuevo nombre del espacio de nombres y 2) evitar la falta de verbosidad y 3) proporcionar abstracción de detalles irrelevantes de la API, mientras que 4) proporciona los mismos diagnósticos y comportamientos beneficiosos del enlazador que proporcionaría el uso de un espacio de nombres no en línea. Digamos que estás usando una biblioteca:

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

Permite al usuario llamar library::foosin necesidad de conocer o incluir la versión ABI en la documentación, que se ve más limpia. Usar library::abiverison129389123::foose vería sucio.

Cuando se realiza una actualización foo, es decir, agregar un nuevo miembro a la clase, no afectará a los programas existentes en el nivel API porque ya no usarán el miembro Y el cambio en el nombre del espacio de nombres en línea no cambiará nada en el nivel API porque library::fooseguirá funcionando

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

Sin embargo, para los programas que se vinculan con él, debido a que el nombre del espacio de nombres en línea se divide en nombres de símbolos como un espacio de nombres normal, el cambio no será transparente para el vinculador. Por lo tanto, si la aplicación no se vuelve a compilar pero está vinculada con una nueva versión de la biblioteca, presentará un símbolo de abi_v1error que no se encuentra, en lugar de que realmente se vincule y luego cause un misterioso error lógico en tiempo de ejecución debido a la incompatibilidad ABI. Agregar un nuevo miembro causará compatibilidad ABI debido al cambio en la definición de tipo, incluso si no afecta el programa en tiempo de compilación (nivel de API).

En este escenario:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

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

Al igual que el uso de 2 espacios de nombres no en línea, permite vincular una nueva versión de la biblioteca sin necesidad de volver a compilar la aplicación, ya abi_v1que se descompondrá en uno de los símbolos globales y utilizará la definición de tipo correcta (antigua). Sin embargo, volver a compilar la aplicación hará que las referencias se resuelvan library::abi_v2.

El uso using namespacees menos funcional que el uso inline(ya que las definiciones fuera de línea no se resuelven) pero proporciona las mismas 4 ventajas que anteriormente. Pero la verdadera pregunta es, ¿por qué seguir usando una solución alternativa cuando ahora hay una palabra clave dedicada para hacerlo? Es una mejor práctica, menos detallada (tiene que cambiar 1 línea de código en lugar de 2) y deja en claro la intención.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.