什么是内联命名空间?

Wal*_*ter 320 c++ namespaces c++11 inline-namespaces

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

(另外,当发生了什么并不清楚,我namespace声明inline在一个但不是所有的声明,这可能住在不同的文件.这难道不是找麻烦?)

Mar*_*utz 329

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

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

举个例子,考虑一下STL的实现vector.如果我们从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
Run Code Online (Sandbox Code Playgroud)

根据值__cplusplus,vector选择一个或另一个实现.如果您的代码库是在C++之前的98次编写的,并且您发现C++ 98版本vector在升级编译器时会给您带来麻烦,那么您需要做的就是找到对所有内容的引用std::vector.您的代码库并替换它们std::pre_cxx_1997::vector.

来的下一个标准,且STL供应商只需再次重复该过程,引入一个新的命名空间std::vectoremplace_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
Run Code Online (Sandbox Code Playgroud)

根据值__cplusplus,我得到一个或另一个实现.

你几乎是正确的.

考虑以下有效的C++ 98用户代码(允许完全专门化stdC++ 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
Run Code Online (Sandbox Code Playgroud)

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

但是:当专门化一个模板时,你需要在它声明的命名空间中这样做.标准说这vector是在命名空间中声明的std,所以这是用户正确期望专门化类型的地方.

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

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

  • 这不是全部.ADL也是一个原因(ADL不会遵循使用指令),也是名称查找.(在名称空间B中使用名称空间A`使命名空间B中的名称隐藏名称空间A中的名称,如果您查找"B :: name" - 内联名称空间不是这样). (45认同)
  • +1用于解释为什么`使用命名空间V99;`在Stroustrup的示例中不起作用. (21认同)
  • @ sasha.sochka,因为在这种情况下你不能使用其他实现.它们将被预处理器删除.使用内联命名空间,您可以通过指定完全限定名称(或`using`关键字)来使用您想要的任何实现. (5认同)
  • 同样地,如果我从头开始一个全新的C++ 21实现,那么我不想在`std :: cxx_11`中实现很多旧的废话.并不是每个编译器都会始终实现标准库的所有旧版本,即使现在很容易认为要求现有实现在添加新版本时留下旧版本是非常小的负担,因为事实上它们都是无论如何.我认为标准可以做的有用的是它是可选的,但如果存在标准名称. (3认同)
  • 为什么不使用`ifdef`s来实现完整的矢量?所有实现都在一个命名空间中,但在预处理之后只会定义其中一个 (3认同)
  • 我认为标准库中的标准版本版本与第三方版本控制相比是有限的.如果C++ 21`vector`对我没有好处,而且我需要C++ 11`vector`,那么这可能是因为C++ 21中的一个重大变化.但这可能是因为我不应该首先依赖的实现细节.该标准不能要求每个C++ 21实现提供一个`std :: cxx_11 :: vector`,它与来自同一供应商的任何过去的`std :: vector`兼容.如果第三方图书馆认为对他们来说是值得的,他们可以承诺这样做. (2认同)
  • @Walter:`g ++ -std = c ++ 11 -x c ++ -dM -E - </ dev/null`表示`__cplusplus`定义为`201103L`.参看 也是16.8 [cpp.predefined]. (2认同)
  • libc ++使用内联命名空间实现标准库以进行版本控制.我认为这是一个完全一致的设计决定,因为我认为它不会破坏任何符合标准的程序.有一些不符合要求的程序,例如那些尝试在std名称空间中转发声明实体的程序. (2认同)

Ste*_*sop 65

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
Run Code Online (Sandbox Code Playgroud)

我不会立即明白你为什么不把using namespace V99;命名空间置于命名空间内Mine,但我不必完全理解用例,以便在委员会的动机中采用Bjarne的话.

  • @walter:他们没有安装`V100.h`,他们正在安装一个名为"Mine"的库."我的"版本99中有3个头文件 - "Mine.h","V98.h"和"V99.h"."我的"版本100中有4个头文件 - "Mine.h","V98.h","V99.h"和"V100.h".头文件的排列是与用户无关的实现细节.如果他们发现一些兼容性问题,这意味着他们需要从他们的部分或全部代码中专门使用`Mine :: V98 :: f`,他们可以调用来自旧代码的`Mine :: V98 :: f`调用在新编写的代码中`Mine :: f`. (5认同)
  • @Walter:在包含`V100.h`的版本中从文件`V99.h`中删除`inline`.当然,你也可以同时修改`Mine.h`来添加额外的包含.`Mine.h`是库的一部分,不是客户端代码的一部分. (2认同)
  • @Walter正如另一个回答所提到的,模板需要专门处理它们声明的命名空间,而不是使用它们声明的命名空间.虽然它看起来很奇怪,但它在那里完成的方式允许你专门化模板`Mine`,而不是专门研究`Mine :: V99`或`Mine :: V98`. (2认同)

Lew*_*sey 10

总结一下要点,using namespace v99两者inline namespace并不相同,前者是在 C++11 中引入专用关键字(inline)之前对版本库的解决方法,它解决了使用问题using,同时提供相同的版本控制功能。使用using namespaceused会导致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> {};
}
Run Code Online (Sandbox Code Playgroud)

这将显示静态分析警告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
}
Run Code Online (Sandbox Code Playgroud)

当使 B 内联时,问题就消失了。

命名空间的其他功能inline是允许库编写者为库提供透明的更新:1) 无需强迫用户使用新的命名空间名称重构代码,2) 防止缺乏冗长性,3) 提供与 API 无关的细节的抽象,同时 4) 提供与使用非内联命名空间相同的有益链接器诊断和行为。假设您正在使用一个库:

namespace library {
    inline namespace abi_v1 {
        class foo {
        } 
    }
}
Run Code Online (Sandbox Code Playgroud)

它允许用户调用library::foo时不需要知道或在文档中包含 ABI 版本,看起来更干净。使用起来library::abiverison129389123::foo会显得很脏。

当更新时foo,即向类添加新成员时,它不会影响 API 级别的现有程序,因为它们不会已经使用该成员,并且内联命名空间名称的更改不会更改 API 级别的任何内容因为library::foo仍然有效。

namespace library {
    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}
Run Code Online (Sandbox Code Playgroud)

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

在这种情况下:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}
Run Code Online (Sandbox Code Playgroud)

与使用 2 个非内联命名空间一样,它允许链接新版本的库,而无需重新编译应用程序,因为它将abi_v1在全局符号之一中被破坏,并且它将使用正确的(旧的)类型定义。然而,重新编译应用程序会导致引用解析为library::abi_v2.

usingusing namespace的功能比 using 少inline(因为超出范围的定义无法解析),但提供了与上面相同的 4 个优点。但真正的问题是,既然现在有一个专用关键字可以做到这一点,为什么还要继续使用解决方法呢?这是更好的实践,不那么冗长(必须更改 1 行代码而不是 2 行)并且使意图清晰。


Mat*_*hew 6

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

\n\n

使用Qt,您可以使用 获得一些额外的、不错的功能Q_ENUM_NS,这反过来又要求封闭的命名空间有一个元对象,该元对象是用 声明的Q_NAMESPACE。但是,为了工作,同一文件中Q_ENUM_NS必须有对应的Q_NAMESPACE \xe2\x81\xbd\xc2\xb9\xe2\x81\xbe并且只能有一个,否则会出现重复定义错误。这实际上意味着所有枚举都必须位于同一标头中。恶心。

\n\n

或者...您可以使用内联名称空间。将枚举隐藏在inline namespace会导致元对象具有不同的损坏名称,同时向用户查找附加名称空间不存在\xe2\x81\xbd\xc2\xb2\xe2\x81\xbe。

\n\n

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

\n\n
\n\n
    \n
  1. 实际上比这更糟糕;它必须位于同一组大括号中。

  2. \n
  3. 除非您尝试在没有完全限定元对象的情况下访问它,但元对象几乎不会直接使用。

  4. \n
\n

  • 不容易。需要单独的命名空间的原因与 Qt 实现细节有关。说实话,很难想象 Qt 之外的情况会有相同的要求。然而,对于这个特定于 Qt 的场景,它们非常有用!有关示例,请参阅 https://gist.github.com/mwoehlke-kitware/bc790dcd474f4b34b812fb34d6a9c8b0 或 https://github.com/Kitware/seal-tk/pull/45。 (2认同)

Mar*_* Ba 6

内联命名空间还可用于提供对命名空间内的功能/名称的细粒度访问。

这是用在std::literals. std 中的命名空间literals都是内联命名空间,因此:

  • 如果您using namespace std;在某个地方使用,您还可以访问 std.h 中的所有用户定义的文字。
  • 但是如果您只需要本地代码中的一组 udl,您也可以这样做,using namespace std::literals::string_literals;并且您将获得在该命名空间中定义的 udl 符号。

对于您想要访问非限定(udl、运算符等)的符号来说,这似乎是一种有用的技术,您可以将它们捆绑在一个内联命名空间中,这样您就可以在该(子)命名空间上进行特定的使用整个库的命名空间。


cod*_*101 5

除了以上所有答案。

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

考虑以下示例:

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

在main.cpp中说

struct bar;
void Foo(bar& ref);
Run Code Online (Sandbox Code Playgroud)

如果在将其编译为对象后检查该文件的符号名称。

$ nm main.o
T__ Z1fooRK6bar 
Run Code Online (Sandbox Code Playgroud)

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

现在,可以将其bar定义为:

struct bar{
   int x;
#ifndef NDEBUG
   int y;
#endif
};
Run Code Online (Sandbox Code Playgroud)

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

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

因此,我们可以这样写:

#ifndef NDEBUG
inline namespace rel { 
#else
inline namespace dbg {
#endif
struct bar{
   int x;
#ifndef NDEBUG
   int y;
#endif
};
}
Run Code Online (Sandbox Code Playgroud)

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

$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar
Run Code Online (Sandbox Code Playgroud)

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

注意符号名称中存在reldbg

现在,如果您尝试将调试与发布模式链接,反之亦然,则将获得一个链接器错误,而不是运行时错误。