Para que servem os namespaces embutidos?

334

O C ++ 11 permite inline namespaces, todos os membros dos quais também estão automaticamente no anexo namespace. Não consigo pensar em nenhuma aplicação útil disso - alguém pode, por favor, dar um exemplo sucinto e breve de uma situação em que inline namespaceé necessário e onde é a solução mais idiomática?

(Além disso, não está claro para mim o que acontece quando a namespaceé declarado inlineem uma, mas não em todas as declarações, que podem estar em arquivos diferentes. Isso não está implorando por problemas?)

Walter
fonte

Respostas:

339

Os espaços para nome em linha são um recurso de versão de biblioteca semelhante ao de versão de símbolo , mas implementado puramente no nível C ++ 11 (por exemplo, plataforma cruzada) em vez de ser um recurso de um formato executável binário específico (por exemplo, específico da plataforma).

É um mecanismo pelo qual o autor de uma biblioteca pode fazer com que um espaço de nome aninhado pareça e atue como se todas as suas declarações estivessem no espaço de nome ao redor (os espaços de nome em linha podem ser aninhados, para que nomes "mais aninhados" cheguem até o primeiro nome não -inclua o espaço para nome e olhe e aja como se suas declarações também estivessem em qualquer um dos espaços para nome).

Como exemplo, considere a implementação de STL de vector. Se tivéssemos espaços para nome embutidos desde o início do C ++, no C ++ 98 o cabeçalho <vector>poderia ter se parecido com o seguinte:

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

Dependendo do valor de __cplusplus, uma ou outra vectorimplementação é escolhida. Se sua base de código foi escrita antes de C ++ 98 vezes e você descobrir que a versão C ++ 98 vectorestá causando problemas ao atualizar seu compilador, "tudo" que você precisa fazer é encontrar as referências std::vectorem sua base de código e substitua-os por std::pre_cxx_1997::vector.

Venha o próximo padrão e o fornecedor STL apenas repita o procedimento novamente, introduzindo um novo espaço para nome std::vectorcom emplace_backsuporte (que requer C ++ 11) e destacando esse iff __cplusplus == 201103L.

OK, então por que preciso de um novo recurso de idioma para isso? Já posso fazer o seguinte para ter o mesmo efeito, não?

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

Dependendo do valor de __cplusplus, recebo uma ou outra das implementações.

E você estaria quase correto.

Considere o seguinte código de usuário válido do C ++ 98 (foi permitido especializar totalmente modelos que já estão no namespace stddo 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

Esse é um código perfeitamente válido, no qual o usuário fornece sua própria implementação de um vetor para um conjunto de tipos em que ela aparentemente conhece uma implementação mais eficiente do que a encontrada na (sua cópia) da STL.

Porém : ao especializar um modelo, é necessário fazê-lo no espaço para nome em que foi declarado. O Padrão diz que vectoré declarado no espaço para nomestd para , e é aí que o usuário espera justamente especializar o tipo.

Esse código funciona com um espaço para nome sem versão stdou com o recurso de espaço para nome embutido no C ++ 11, mas não com o truque de versão usado using namespace <nested>, porque expõe os detalhes da implementação que o verdadeiro espaço para nome no qual vectorfoi definido não foi stddiretamente.

Existem outros buracos pelos quais você pode detectar o espaço para nome aninhado (consulte os comentários abaixo), mas os espaços para nome embutidos conectam todos eles. E é só isso. Imensamente útil para o futuro, mas o AFAIK the Standard não prescreve nomes de namespace em linha para sua própria biblioteca padrão (eu adoraria provar que está errado nisso), portanto, ele só pode ser usado para bibliotecas de terceiros, não o próprio padrão (a menos que os fornecedores do compilador concordem com um esquema de nomenclatura).

Marc Mutz - mmutz
fonte
23
+1 para explicar por using namespace V99;que não funciona no exemplo de Stroustrup.
Steve Jessop
3
E da mesma forma, se eu iniciar uma nova implementação do C ++ 21 do zero, não quero ser sobrecarregada ao implementar muitas bobagens antigas std::cxx_11. Nem todo compilador sempre implementará todas as versões antigas das bibliotecas padrão, embora seja tentador, no momento, pensar que seria muito pouco fardo exigir que as implementações existentes deixem as antigas quando adicionam as novas, pois na verdade todas elas são assim mesmo. Suponho que o que o padrão poderia ter sido útil o torne opcional, mas com um nome padrão, se presente.
21812 Steve
46
Não é só isso. A ADL também foi um motivo (a ADL não seguirá o uso de diretivas) e a pesquisa de nome também. ( using namespace Aem um espaço para nome B, os nomes no espaço para nome B ocultam nomes no espaço para nome A, se você procurar B::name- não com espaços para nome embutidos).
Johannes Schaub - litb 15/06
4
Por que não usar ifdefs para a implementação completa do vetor? Todas as implementações seria em um espaço de nomes, mas apenas um deles será definido após o pré-processamento
sasha.sochka
6
@ sasha.sochka, porque neste caso você não pode usar outras implementações. Eles serão removidos pelo pré-processador. Com espaços para nome embutidos, você pode usar qualquer implementação desejada especificando um nome completo (ou usingpalavra-chave).
28813 Vasily Biryukov
70

http://www.stroustrup.com/C++11FAQ.html#inline-namespace (um documento escrito e mantido por Bjarne Stroustrup, que você acha que deveria estar ciente da maioria das motivações para a maioria dos recursos do C ++ 11. )

De acordo com isso, é para permitir versões para compatibilidade com versões anteriores. Você define vários espaços para nome internos e cria o mais recente inline. De qualquer forma, o padrão para pessoas que não se importam com o controle de versão. Suponho que a mais recente possa ser uma versão futura ou de ponta que ainda não seja o padrão.

O exemplo dado é:

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

Não percebo imediatamente por que você não using namespace V99;insere um espaço para nome Mine, mas não preciso entender completamente o caso de uso para que a palavra de Bjarne seja motivada pelo comitê.

Steve Jessop
fonte
Então, na verdade, a última f(1)versão seria chamada a partir do V99namespace embutido ?
Eitan T
11
@EitanT: sim, porque o espaço para nome global possui using namespace Mine;e o Mineespaço para nome contém tudo, desde o espaço para nome embutido Mine::V99.
21812 Steve Jobs
2
@ Walter: você remove inlinedo arquivo V99.hna versão que inclui V100.h. Você também modifica Mine.hao mesmo tempo, é claro, para adicionar uma inclusão extra. Mine.hfaz parte da biblioteca, não faz parte do código do cliente.
21812 Steve Jobs
5
@walter: eles não estão instalando V100.h, estão instalando uma biblioteca chamada "Mine". Existem 3 arquivos de cabeçalho na versão 99 de "Mine" - Mine.h, V98.he V99.h. Existem 4 arquivos de cabeçalho na versão 100 de "Mine" - Mine.h, V98.h, V99.he V100.h. A organização dos arquivos de cabeçalho é um detalhe de implementação que é irrelevante para os usuários. Se eles descobrirem algum problema de compatibilidade, o que significa que precisam usar especificamente Mine::V98::fde parte ou de todo o código, eles poderão misturar chamadas Mine::V98::fdo código antigo com chamadas Mine::fno código recém-escrito.
21812 Steve Jobs (
2
@Walter Como a outra resposta menciona, os modelos precisam ser especializados no espaço para nome em que estão declarados, e não um espaço para nomes usando aquele em que estão declarados. Embora pareça estranho, a maneira como é feita lá permite que você se especialize em modelos. Mine, em vez de precisar se especializar em Mine::V99ou Mine::V98.
Justin Time - Restabelece Monica
8

Além de todas as outras respostas.

O espaço para nome embutido pode ser usado para codificar informações da ABI ou Versão das funções nos símbolos. É por esse motivo que eles são usados ​​para fornecer compatibilidade ABI com versões anteriores. Os espaços para nome em linha permitem injetar informações no nome desconfigurado (ABI) sem alterar a API, pois afetam apenas o nome do símbolo do vinculador.

Considere este exemplo:

Suponha que você escreva uma função Fooque faça referência a um objeto say bare não retorne nada.

Diga em main.cpp

struct bar;
void Foo(bar& ref);

Se você verificar o nome do seu símbolo para esse arquivo depois de compilá-lo em um objeto.

$ nm main.o
T__ Z1fooRK6bar 

O nome do símbolo do vinculador pode variar, mas certamente codificará o nome dos tipos de função e argumento em algum lugar.

Agora, pode ser que barseja definido como:

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

Dependendo do tipo de compilação, barpode se referir a dois tipos / layouts diferentes com os mesmos símbolos do vinculador.

Para evitar esse comportamento, envolvemos nossa estrutura barem um espaço de nome embutido, onde, dependendo do tipo de compilação, o símbolo do vinculador barserá diferente.

Então, poderíamos escrever:

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

Agora, se você olhar para o arquivo de objeto de cada objeto, crie um usando release e outro com o sinalizador de depuração. Você descobrirá que os símbolos do vinculador também incluem o nome do espaço para nome embutido. Nesse caso

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

Os nomes dos símbolos do vinculador podem ser diferentes.

Observe a presença rele dbgnos nomes dos símbolos.

Agora, se você tentar vincular a depuração ao modo de liberação ou vice-versa, receberá um erro do vinculador como contrário ao erro de tempo de execução.

coder3101
fonte
11
Sim, isso faz sentido. Portanto, isso é mais para implementadores de bibliotecas e similares.
Walter Walter
3

Na verdade, descobri outro uso para namespaces embutidos.

Com o Qt , você obtém alguns recursos extras e agradáveis Q_ENUM_NS, que, por sua vez, exigem que o espaço para nome anexo tenha um meta-objeto, que é declarado com Q_NAMESPACE. No entanto, Q_ENUM_NSpara funcionar, é necessário que haja um correspondente Q_NAMESPACE no mesmo arquivo ⁽¹⁾. E só pode haver um, ou você recebe erros de definição duplicados. Isso significa efetivamente que todas as suas enumerações precisam estar no mesmo cabeçalho. Que nojo.

Ou ... você pode usar espaços para nome embutidos. Ocultar enumerações em uminline namespacefaz com que os meta-objetos tenham diferentes nomes desconfigurados, enquanto olhar para os usuários como o espaço para nome adicional não existe⁽²⁾.

Portanto, eles são úteis para dividir coisas em vários sub-namespaces que se parecem com um namespace, se você precisar fazer isso por algum motivo. Obviamente, isso é semelhante à escrita using namespace innerno espaço para nome externo, mas sem a violação DRY de escrever o nome do espaço para nome interno duas vezes.


  1. Na verdade, é pior que isso; deve estar no mesmo conjunto de chaves.

  2. A menos que você tente acessar o meta-objeto sem qualificá-lo completamente, mas o meta-objeto quase nunca é usado diretamente.

Mateus
fonte
Você pode esboçar isso com um esqueleto de código? (idealmente sem referência explícita a Qt). Tudo parece bastante envolvido / pouco claro.
Walter
Não facilmente. A razão pela qual são necessários espaços para nome separados está relacionada aos detalhes da implementação do Qt. TBH, é difícil imaginar uma situação fora do Qt que tivesse os mesmos requisitos. No entanto, nesse cenário específico do Qt, eles são úteis! Veja gist.github.com/mwoehlke-kitware/… ou github.com/Kitware/seal-tk/pull/45 para um exemplo.
Mateus
0

Portanto, para resumir os pontos principais, using namespace v99e inline namespacenão eram os mesmos, o primeiro era uma solução alternativa para as bibliotecas de versões antes de uma palavra-chave dedicada (inline) ser introduzida no C ++ 11, que corrigia os problemas de uso usinge fornecia a mesma funcionalidade de versão. O uso de using namespaceusado para causar problemas com o ADL (embora o ADL agora pareça seguir as usingdiretrizes) e a especialização fora de linha de uma classe / função da biblioteca etc. pelo usuário não funcionariam se fossem feitas fora do namespace verdadeiro (cujo nome o o usuário não saberia e não deveria saber, ou seja, o usuário teria que usar B :: abi_v2 :: em vez de apenas B :: para a especialização resolver).

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

Isso mostrará um aviso de análise estática first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]. Mas se você criar o espaço para nome A embutido, o compilador resolverá corretamente a especialização. Embora, com as extensões do C ++ 11, o problema desapareça.

Definições fora de linha não resolvem ao usar using; eles precisam ser declarados em um bloco de namespace de extensão aninhada / não aninhada (o que significa que o usuário precisa conhecer a versão ABI novamente, se por qualquer motivo, tiver permissão para fornecer sua própria implementação de uma função).

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

O problema desaparece ao criar o B inline.

Os outros inlinenamespaces de funcionalidade possuem permitem que o gravador da biblioteca forneça uma atualização transparente para a biblioteca 1) sem forçar o usuário a refatorar o código com o novo nome do namespace e 2) evitando falta de verbosidade e 3) fornecendo abstração de detalhes irrelevantes da API, enquanto 4) fornecer o mesmo diagnóstico e comportamento benéfico do vinculador que o uso de um espaço para nome não embutido forneceria. Digamos que você esteja usando uma biblioteca:

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

Ele permite que o usuário ligue library::foosem precisar saber ou incluir a versão ABI na documentação, que parece mais limpa. Usar library::abiverison129389123::foopareceria sujo.

Quando uma atualização é feita para foo, ou seja, adicionar um novo membro à classe, ela não afeta os programas existentes no nível da API, porque eles ainda não estão usando o membro E a alteração no nome do namespace embutido não altera nada no nível da API. porque library::fooainda vai funcionar.

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

No entanto, para programas vinculados a ele, porque o nome do espaço para nome embutido é dividido em nomes de símbolo como um espaço para nome regular, a alteração não será transparente para o vinculador. Portanto, se o aplicativo não for recompilado, mas estiver vinculado a uma nova versão da biblioteca, ele apresentará um abi_v1erro de erro não encontrado, em vez de vincular e causar um erro lógico misterioso no tempo de execução devido à incompatibilidade ABI. A adição de um novo membro causará compatibilidade com a ABI devido à alteração na definição de tipo, mesmo que não afete o programa no momento da compilação (nível da API).

Neste cenário:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

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

Assim como o uso de dois namespaces não embutidos, ele permite que uma nova versão da biblioteca seja vinculada sem a necessidade de recompilar o aplicativo, porque abi_v1será confundida com um dos símbolos globais e usará a definição de tipo correta (antiga). No entanto, recompilar o aplicativo faria com que as referências fossem resolvidas library::abi_v2.

Usar using namespaceé menos funcional do que usar inline(pois as definições fora de linha não resolvem), mas oferece as mesmas 4 vantagens acima. Mas a verdadeira questão é: por que continuar usando uma solução alternativa quando agora existe uma palavra-chave dedicada para fazer isso? É uma prática melhor, menos detalhada (precisa alterar uma linha de código em vez de 2) e torna clara a intenção.

Lewis Kelsey
fonte