什么是内联命名空间?

时间:2012-06-13 13:46:14

标签: c++ namespaces c++11 inline-namespaces

C ++ 11允许inline namespace,其所有成员也自动位于封闭的namespace中。我想不出任何有用的应用 - 有人可以给出一个简短,简洁的例子,说明需要inline namespace的情况以及它是最惯用的解决方案吗?

(另外,我不清楚当namespace在一个但不是所有声明中声明inline时会发生什么,这些声明可能存在于不同的文件中。这不是乞求麻烦吗? )

5 个答案:

答案 0 :(得分:315)

内联名称空间是类似于symbol versioning的库版本控制功能,但纯粹在C ++ 11级别(即跨平台)实现,而不是特定二进制可执行格式(即平台)的功能特异性)。

这是一种机制,通过该机制,库作者可以使嵌套的命名空间看起来并且就好像它的所有声明都在周围的命名空间中一样(内联命名空间可以嵌套,因此“更多嵌套”的名称一直渗透到第一个非内联命名空间,看起来和行为就好像它们的声明也在它们之间的任何命名空间中。)

作为示例,请考虑vector的STL实现。如果我们从C ++开头有内联命名空间,那么在C ++ 98中,标题<vector>可能看起来像这样:

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

根据__cplusplus的值,选择一个或另一个vector实现。如果您的代码库是在C ++之前的98次编写的,并且您发现vector的C ++ 98版本在升级编译器时给您带来麻烦,那么您需要做的就是“全部”在代码库中找到对std::vector的引用,并将其替换为std::pre_cxx_1997::vector

采用下一个标准,STL供应商再次重复该过程,为std::vector引入一个新的命名空间,其emplace_back支持(需要C ++ 11)并且内联一个iff {{ 1}}。

好的,为什么我需要一个新的语言功能呢?我已经可以做以下同样的效果,不是吗?

__cplusplus == 201103L

根据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 的值,我得到一个或另一个实现。

你几乎是正确的。

考虑以下有效的C ++ 98用户代码(允许完全专门化C ++ 98中名称空间__cplusplus中的模板):

std

这是完全有效的代码,用户为一组类型提供自己的向量实现,在那里她显然知道比STL(她的副本)中找到的更有效的实现。

:在专门化模板时,需要在声明它的名称空间中执行此操作。标准说// 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 在名称空间vector中声明,所以这是用户合理期望专门化类型的地方。

此代码适用于非版本化命名空间std,或使用C ++ 11内联命名空间功能,但不适用于使用std的版本控制技巧,因为它公开了实现细节, using namespace <nested>定义的真正名称空间不是直接vector

您可以通过其他漏洞检测嵌套命名空间(请参阅下面的注释),但内联命名空间会将它们全部插入。这就是它的全部内容。对于未来非常有用,但AFAIK标准没有规定其自己的标准库的内联命名空间名称(尽管我喜欢被证明是错误的),因此它只能用于第三方库,而不是标准本身(除非编译器供应商同意命名方案)。

答案 1 :(得分:61)

http://www.stroustrup.com/C++11FAQ.html#inline-namespace(由Bjarne Stroustrup编写并维护的文档,您认为应该了解大多数C ++ 11功能的大多数动机。)

据此,它允许版本化以实现向后兼容性。您可以定义多个内部名称空间,并创建最新的名称空间inline。或者无论如何,对于不关心版本控制的人来说是默认的。我想最新的版本可能是未来或最新版本,但尚未默认。

给出的例子是:

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

我没有立即明白为什么你不把using namespace V99;放在命名空间Mine里面,但我不必完全理解用例,以便把Bjarne的话放在它上面委员会的动机。

答案 2 :(得分:2)

除了以上所有答案。

内联名称空间可用于编码符号中的ABI信息或功能的版本。由于这个原因,它们被用来提供向后的ABI兼容性。内联名称空间使您可以将信息注入到整齐的名称(ABI)中,而无需更改API,因为它们仅影响链接器符号名称。

考虑以下示例:

假设您编写了一个函数Foo,该函数引用了一个对象bar,但未返回任何内容。

在main.cpp中说

struct bar;
void Foo(bar& ref);

如果将此文件编译为对象后检查符号名称。

$ nm main.o
T__ Z1fooRK6bar 
  

链接器符号名称可能会有所不同,但是肯定会在某处对函数名称和参数类型进行编码。

现在,可能bar被定义为:

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

根据构建类型,bar可以引用具有相同链接器符号的两种不同类型/布局。

为防止此类行为,我们将结构bar包装到一个内联名称空间中,根据构建类型,bar的链接器符号将有所不同。

所以,我们可以这样写:

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

现在,如果您查看每个对象的对象文件,则可以使用release构建一个对象,使用调试标志构建另一个对象。您会发现链接器符号也包括内联名称空间名称。在这种情况下

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

链接器符号名称可能不同。

请注意,符号名称中包含reldbg

现在,如果您尝试将调试与发布模式或反之亦然链接起来,则会得到一个链接器错误,与运行时错误相反。

答案 3 :(得分:1)

我实际上发现了内联名称空间的另一种用途。

使用Qt,您可以使用Q_ENUM_NS获得一些额外的,不错的功能,这又要求封闭的名称空间具有一个用Q_NAMESPACE声明的元对象。但是,为了使Q_ENUM_NS工作,同一文件中必须有一个相应的Q_NAMESPACE ⁽¹⁽。而且只能有一个,否则会出现重复的定义错误。实际上,这意味着所有枚举都必须位于同一标头中。好吧。

或... ,您可以使用内联名称空间。隐藏inline namespace中的枚举会导致元对象具有不同的名称,而像其他用户一样,在查找用户时则不存在。

因此,如果您出于某种原因需要这样做,它们对于将内容拆分为多个外观就像一个名称空间的子名称空间很有用。当然,这类似于在外部名称空间中写入using namespace inner,但没有DRY违反两次写入内部名称空间名称的情况。


  1. 实际上比那更糟;它必须在同一组大括号中。

  2. 除非您尝试在不完全限定其访问权限的情况下访问该元对象,但是几乎从未直接使用该元对象。

答案 4 :(得分:0)

因此,总结一下using namespace v99inline namespace的要点,在C ++ 11中引入专用关键字(内联)之前,前者是版本库的变通方法。解决了使用using的问题,同时提供了相同的版本控制功能。使用曾经引起ADL问题的using namespace(尽管ADL现在似乎遵循了using指令),并且如果用户无法对库类/函数等进行离线专业化,则无法使用在真正的名称空间之外完成操作(用户不知道也不应该知道其名称,即用户必须使用B :: abi_v2 ::而不是仅使用B ::才能解决专业化问题)。

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

这将显示静态分析警告first declaration of class template specialization of 'myclass' outside namespace 'A' is a C++11 extension [-Wc++11-extensions]。但是,如果将内联名称空间设为A,则编译器会正确解析该专业化名称。尽管有了C ++ 11扩展,问题仍然消失了。

使用using时,行外定义无法解析;必须在嵌套/非嵌套扩展名称空间块中声明它们(这意味着,如果出于任何原因允许用户提供自己的函数实现,则用户需要再次知道ABI版本)。

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

使B内联时问题消失。

命名空间inline的其他功能是允许库编写器提供对库的透明更新1)无需强制用户使用新的命名空间名称重构代码; 2)避免冗长; 3)提供与API不相关的详细信息的抽象,而4)提供与使用非内联名称空间相同的有益的链接器诊断和行为。假设您正在使用一个库:

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

它使用户可以调用library::foo,而无需了解ABI版本或将其包含在文档中,该文档看上去更干净。使用library::abiverison129389123::foo看起来很脏。

当对foo进行更新时,即向类中添加新成员,这将不会影响API级别的现有程序,因为它们将不再使用该成员并且更改内联名称空间名称不会在API级别进行任何更改,因为library::foo仍然可以使用。

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

但是,对于与之链接的程序,由于内联名称空间名称像常规名称空间一样被改编为符号名称,因此更改对链接器而言不是透明的。因此,如果未重新编译应用程序,但将其与库的新版本链接,则它将显示未发现错误的符号abi_v1,而不是实际链接,然后由于以下原因在运行时导致神秘的逻辑错误: ABI不兼容。添加新成员会由于类型定义的更改而导致ABI兼容性,即使它在编译时(API级别)不影响程序也是如此。

在这种情况下:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

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

就像使用2个非内联名称空间一样,它允许链接库的新版本,而无需重新编译应用程序,因为abi_v1将被替换为一个全局符号,并且它将使用正确的(旧的)类型定义。但是,重新编译应用程序将导致引用解析为library::abi_v2

使用using namespace的功能比使用inline的功能少(因为无法定义行外定义),但提供了与上述相同的4个优点。但是真正的问题是,当现在有专用关键字来执行此操作时,为什么要继续使用替代方法。更好的做法是,减少冗长的内容(必须更改1行代码而不是2行),并使意图明确。