内联命名空间有什么用?

What are inline namespaces for?

提问人:Walter 提问时间:6/13/2012 最后编辑:Marc Mutz - mmutzWalter 更新时间:2/2/2022 访问量:79405

问:

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

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

C++ C++11 内联命名空间

评论


答:

88赞 Steve Jessop 6/13/2012 #1

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

我没有立即明白你为什么不把 命名空间 ,但我不必完全理解用例就可以接受 Bjarne 的话作为委员会的动机。using namespace V99;Mine

评论

0赞 Eitan T 6/13/2012
所以实际上最后一个版本会从内联命名空间调用吗?f(1)V99
1赞 Steve Jessop 6/13/2012
@EitanT:是的,因为全局命名空间有 ,并且命名空间包含内联命名空间中的所有内容。using namespace Mine;MineMine::V99
2赞 Steve Jessop 6/13/2012
@Walter:从包含 .当然,您也要同时进行修改,以添加额外的包含。 是库的一部分,而不是客户端代码的一部分。inlineV99.hV100.hMine.hMine.h
5赞 Steve Jessop 6/13/2012
@walter:他们没有安装,他们正在安装一个名为“Mine”的库。“Mine”版本 99 中有 3 个头文件-- ,和 .“Mine”版本 100 中有 4 个头文件-- , ,和 .头文件的排列是与用户无关的实现细节。如果他们发现一些兼容性问题,这意味着他们需要专门使用部分或全部代码,他们可以将对旧代码的调用与对新编写代码的调用混合在一起。V100.hMine.hV98.hV99.hMine.hV98.hV99.hV100.hMine::V98::fMine::V98::fMine::f
3赞 Justin Time - Reinstate Monica 3/13/2016
@Walter 正如另一个答案所提到的,模板需要专门用于声明它们的命名空间,而不是使用声明它们的命名空间。虽然它看起来很奇怪,但它的完成方式允许您将模板专攻为 ,而不必专攻 或 。MineMine::V99Mine::V98
402赞 Marc Mutz - mmutz 6/13/2012 #2

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

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

例如,考虑 的 STL 实现。如果我们从 C++ 开始就有内联命名空间,那么在 C++98 中,标头可能如下所示:vector<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

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

到了下一个标准,STL 供应商只是再次重复该过程,为 with support(需要 C++11)引入一个新的命名空间并内联该命名空间 iff .std::vectoremplace_back__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 中的实现更有效的实现。

但是:在专用模板时,您需要在声明模板的命名空间中执行此操作。标准说这是在命名空间中声明的,所以这是用户理所当然地期望专门化类型的地方。vectorstd

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

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

评论

27赞 Steve Jessop 6/14/2012
+1 用于解释为什么在 Stroustrup 的例子中不起作用。using namespace V99;
3赞 Steve Jessop 6/14/2012
同样,如果我从头开始一个全新的 C++21 实现,那么我不想在 .并非每个编译器都会始终实现标准库的所有旧版本,尽管目前人们很容易认为,要求现有实现在添加新库时保留旧版本的负担很小,因为实际上它们都是如此。我想该标准可以做的是将其设置为可选,但如果存在,则具有标准名称。std::cxx_11
54赞 Johannes Schaub - litb 6/15/2012
这还不是全部。ADL 也是一个原因(ADL 不会遵循使用指令),名称查找也是如此。(在命名空间 B 中,如果查找,则在命名空间 B 中隐藏名称 - 内联命名空间则不然)。using namespace AB::name
5赞 sasha.sochka 9/8/2013
为什么不直接使用 s 来实现完整的向量?所有实现都将在一个命名空间中,但在预处理后只定义其中一个ifdef
7赞 Vasily Biryukov 11/28/2013
@sasha.sochka,因为在这种情况下,您不能使用其他实现。它们将被预处理器删除。使用内联命名空间,可以通过指定完全限定名称(或关键字)来使用所需的任何实现。using
29赞 coder3101 6/21/2019 #3

除了所有其他答案。

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

请看这个例子:

假设您编写了一个函数,该函数引用了对某个对象的引用,并且不返回任何内容。Foobar

在 main.cpp 中说

struct bar;
void Foo(bar& ref);

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

$ nm main.o
T__ Z1fooRK6bar 

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

现在,它可以定义为:bar

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

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

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

所以,我们可以这样写:

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

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

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

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

请注意符号名称中存在 和 。reldbg

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

评论

1赞 Walter 6/22/2019
是的,这是有道理的。因此,这更适合库实现者等。
6赞 Matthew 8/28/2019 #4

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

使用Qt,你可以使用Q_ENUM_NS获得一些额外的、不错的功能,这反过来又要求封闭的命名空间有一个元对象,该元对象是用 .但是,为了工作,同一文件中必须有一个对应的文件⁽¹⁾。而且只能有一个,否则你会得到重复的定义错误。实际上,这意味着所有枚举都必须位于同一标头中。呸。Q_NAMESPACEQ_ENUM_NSQ_NAMESPACE

或。。。您可以使用内联命名空间。在 中隐藏枚举会导致元对象具有不同的错误名称,而对用户来说,就像其他命名空间不存在⁽²⁾一样。inline namespace

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


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

  2. 除非您尝试在不完全限定元对象的情况下访问元对象,否则元对象几乎从未被直接使用过。

评论

1赞 Walter 8/28/2019
你能用代码的骨架来勾勒它吗?(理想情况下,没有明确引用 Qt)。这一切听起来相当复杂/不清楚。
2赞 Matthew 8/29/2019
不。。。容易。需要单独的命名空间的原因与Qt实现细节有关。TBH,很难想象在Qt之外的情况会有同样的要求。但是,对于这种特定于 Qt 的场景,它们非常有用!有关示例,请参阅 gist.github.com/mwoehlke-kitware/...github.com/Kitware/seal-tk/pull/45
10赞 Lewis Kelsey 3/28/2020 #5

因此,总结要点,并且并不相同,前者是在 C++11 中引入专用关键字(内联)之前版本库的解决方法,它修复了使用 的问题,同时提供相同的版本控制功能。如果在真正的命名空间之外完成,则使用用于导致 ADL 问题(尽管 ADL 现在似乎遵循指令),并且用户对库类/函数等进行行外专用化将不起作用(用户不会也不应该知道其名称,即用户将不得不使用 B::abi_v2:: 而不仅仅是 B:: 以解决专业化问题)。using namespace v99inline namespaceusingusing namespaceusing

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

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

使用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 内联时,问题消失了。

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

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

它允许用户调用,而无需知道或在文档中包含 ABI 版本,这看起来更简洁。使用会看起来很脏。library::foolibrary::abiverison129389123::foo

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

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

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

在此方案中:

namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

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

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

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

6赞 Martin Ba 2/2/2022 #6

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

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

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

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