【问题标题】:What are inline namespaces for?什么是内联命名空间?
【发布时间】:2012-06-16 11:45:30
【问题描述】:

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

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

【问题讨论】:

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


    【解决方案1】:

    内联命名空间是类似于 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 __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
    

    根据__cplusplus 的值,我会得到其中一个实现。

    你几乎是正确的。

    考虑以下有效的 C++98 用户代码(已经允许在 C++98 中完全特化位于命名空间 std 中的模板):

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

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

    但是:当特化一个模板时,你需要在它被声明的命名空间中这样做。标准说 vector 在命名空间 std 中声明,所以这就是用户所在的位置理所当然地期望专门化该类型。

    此代码可与非版本化命名空间 std 或 C++11 内联命名空间功能一起使用,但不适用于使用 using namespace &lt;nested&gt; 的版本控制技巧,因为这会暴露真正命名空间所在的实现细节vector 的定义不是 std 直接定义的。

    还有其他漏洞可以用来检测嵌套命名空间(请参阅下面的 cmets),但内联命名空间会将它们全部塞住。这就是它的全部。对未来非常有用,但 AFAIK 标准并没有为它自己的标准库规定内联命名空间名称(不过我很想被证明是错误的),所以它只能用于第三方库,而不是标准本身(除非编译器供应商同意命名方案)。

    【讨论】:

    • +1 用于解释为什么 using namespace V99; 在 Stroustrup 的示例中不起作用。
    • 同样,如果我从头开始一个全新的 C++21 实现,那么我不想在 std::cxx_11 中实现很多旧的废话。并非每个编译器都会始终实现标准库的所有旧版本,尽管目前很容易认为在添加新实现时要求现有实现保留旧版本是非常小的负担,因为事实上它们都反正都是。我想该标准可以做的有用的事情是使其成为可选的,但如果存在标准名称。
    • 这还不是全部。 ADL 也是一个原因(ADL 不会遵循 using 指令),以及名称查找。 (命名空间 B 中的using namespace A 使命名空间 B 中的名称隐藏命名空间 A 中的名称,如果您查找 B::name - 内联命名空间则不然)。
    • 为什么不直接使用ifdefs 来实现全向量呢?所有实现都将在一个命名空间中,但在预处理后只会定义其中一个
    • @sasha.sochka,因为在这种情况下您不能使用其他实现。它们将被预处理器删除。使用内联命名空间,您可以通过指定完全限定名称(或 using 关键字)来使用您想要的任何实现。
    【解决方案2】:

    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 的话。

    【讨论】:

    • 所以实际上最后一个f(1) 版本会从内联V99 命名空间中调用?
    • @EitanT:是的,因为全局命名空间有using namespace Mine;,而Mine 命名空间包含内联命名空间Mine::V99 中的所有内容。
    • @Walter:您在包含V100.h 的版本中从文件V99.h 中删除inline。当然,您还同时修改Mine.h,以添加额外的包含。 Mine.h 是库的一部分,而不是客户端代码的一部分。
    • @walter:他们不是在安装V100.h,而是在安装一个名为“Mine”的库。 “我的”99版中有3个头文件——Mine.hV98.hV99.h。 “我的”100版中有4个头文件——Mine.hV98.hV99.hV100.h。头文件的排列是与用户无关的实现细节。如果他们发现一些兼容性问题,这意味着他们需要从部分或全部代码中专门使用 Mine::V98::f,他们可以将旧代码中对 Mine::V98::f 的调用与新编写的代码中对 Mine::f 的调用混合使用。
    • @Walter 正如另一个答案所提到的,模板需要专门用于它们声明的命名空间,而不是使用它们声明的命名空间的命名空间。虽然它看起来很奇怪,但它的方式在那里完成后,您可以专门针对 Mine 的模板,而不必专门针对 Mine::V99Mine::V98
    【解决方案3】:

    除了所有其他答案。

    内联命名空间可用于编码 ABI 信息或符号中函数的版本。正是由于这个原因,它们被用来提供向后 ABI 兼容性。内联命名空间允许您在不改变 API 的情况下将信息注入错位名称 (ABI),因为它们仅影响链接器符号名称。

    考虑这个例子:

    假设你编写了一个函数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
    };
    }
    

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

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

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

    注意符号名称中存在reldbg

    现在,如果您尝试将调试与发布模式或反之亦然链接,您将收到与运行时错误相反的链接器错误。

    【讨论】:

    • 是的,这是有道理的。所以这更适用于库实现者等。
    【解决方案4】:

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

    使用Qt,您可以使用Q_ENUM_NS 获得一些额外的、不错的功能,这反过来又要求封闭的命名空间有一个用Q_NAMESPACE 声明的元对象。然而,为了使Q_ENUM_NS 工作,必须有一个对应的Q_NAMESPACE 在同一个文件中⁽¹⁾。而且只能有一个,否则会出现重复的定义错误。这实际上意味着您的所有枚举都必须在同一个标​​题中。呸。

    或者...您可以使用内联命名空间。将枚举隐藏在 inline namespace 中会导致元对象具有不同的错位名称,而在用户看来,额外的命名空间不存在⁽²⁾。

    因此,如果您出于某种原因需要这样做,它们对于将内容拆分成多个子命名空间很有用,这些子命名空间看起来都像一个命名空间。当然,这类似于在外层命名空间中写using namespace inner,但没有DRY违反两次写内层命名空间的名称。


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

    2. 除非您尝试访问元对象而不对其进行完全限定,但元对象几乎不会直接使用。

    【讨论】:

    • 你能用代码框架来勾勒它吗? (理想情况下不明确引用 Qt)。这一切听起来相当复杂/不清楚。
    • 不容易。需要单独命名空间的原因与 Qt 实现细节有关。 TBH,很难想象 Qt 之外的情况也会有相同的要求。然而,对于这个特定于 Qt 的场景,它们非常有用!有关示例,请参阅 gist.github.com/mwoehlke-kitware/…github.com/Kitware/seal-tk/pull/45
    【解决方案5】:

    所以总结一下要点,using namespace v99inline namespace 是不一样的,前者是在 C++11 中引入专用关键字(inline)之前版本库的一种解决方法,它解决了以下问题使用using,同时提供相同的版本控制功能。使用 using namespace 会导致 ADL 出现问题(尽管 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 not being found 错误,而不是实际链接然后由于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 行)并使意图明确。

    【讨论】:

      猜你喜欢
      • 2013-12-11
      • 2011-03-23
      • 1970-01-01
      • 2011-04-24
      • 2020-02-19
      相关资源
      最近更新 更多