内联命名空间有什么作用?

422

C++11允许使用inline namespace,其中所有成员也自动属于封闭的namespace。我无法想出任何有用的应用场景-是否可以给出一个简洁明了的例子,说明何时需要使用inline namespace以及它是最通用的解决方案?

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

6个回答

402

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

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

例如,考虑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

根据__cplusplus的值,选择其中一个vector实现。如果您的代码库是在C++98之前编写的,并且在升级编译器时发现C++98版本的vector给您带来了麻烦,“所有”您需要做的就是找到代码库中对std::vector的引用,并将其替换为std::pre_cxx_1997::vector
下一个标准将重复上述过程,为std::vector引入一个新的命名空间,支持emplace_back(需要C++11),并在__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 <nested>的版本控制技巧,因为它暴露了vector真正被定义的名称空间不是直接std的实现细节。
还有其他漏洞可以检测到嵌套命名空间(请参见下面的注释),但内联命名空间可以修补所有这些漏洞。就是这样。对于未来来说非常有用,但据我所知,标准并未指定其自己的标准库的内联命名空间名称(虽然我很想被证明是错误的),因此它只能用于第三方库,而不能用于标准库本身(除非编译器供应商同意命名方案)。

27
+1 是因为该用语句无法在 Stroustrup 的示例中正常工作。 - Steve Jessop
3
同样地,如果我要从头开始实现一个全新的 C++21,那么我不想被旧版本的 std::cxx_11 给拖累。并非每个编译器都会一直实现所有旧版本的标准库,即使此时认为要求现有实现在添加新功能时保留旧功能开销很小,但实际上它们本身就已经有了。我想标准可以有用地做的是将其变成可选项,并且如果存在,则使用标准名称。 - Steve Jessop
54
不只是这样,ADL也是一个原因(在使用指示符时ADL不会跟随),名称查找也是如此。在命名空间B中使用命名空间A的using语句会使得在寻找B::name时,将会隐藏命名空间A中的同名对象,但是在内联命名空间中则不会。 - Johannes Schaub - litb
5
为什么不只是使用#ifdef来实现完整的向量实现?所有实现将位于一个命名空间中,但只有在预处理后定义其中一个。 - sasha.sochka
7
@sasha.sochka,因为在这种情况下,您不能使用其他实现。它们将由预处理器删除。通过使用内联命名空间,您可以通过指定完全限定名称(或 using 关键字)来使用任何要使用的实现。 - Vasily Biryukov
显示剩余18条评论

88

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 命名空间中调用? - Eitan T
1
@EitanT:是的,因为全局命名空间有“using namespace Mine;”,而“Mine”命名空间包含来自内联命名空间“Mine::V99”的所有内容。 - Steve Jessop
2
@Walter:在包含 V100.h 的发布版中,你从文件 V99.h 中删除了 inline。当然,同时你也修改了 Mine.h,添加了一个额外的 include。Mine.h 是库的一部分,而不是客户端代码的一部分。 - Steve Jessop
5
@walter说他们并没有安装V100.h,他们在安装一个名为“Mine”的库。在“Mine”版本99中有3个头文件-- Mine.h, V98.hV99.h。在“Mine”版本100中有4个头文件-- Mine.h, V98.h, V99.hV100.h。这些头文件的排列顺序是一个与用户无关的实现细节。如果他们发现某些兼容性问题,需要从他们的一些或所有代码中特别使用Mine::V98::f,他们可以将旧代码中调用Mine::V98::f和新编写的代码中的Mine::f混合调用。 - Steve Jessop
3
正如其他答案所提到的,模板需要在它们声明的命名空间内进行专门化,而不是在使用它们声明的命名空间中进行专门化。虽然看起来有点奇怪,但这种方式允许您在 Mine 中专门化模板,而不必在 Mine::V99Mine::V98 中进行专门化。 - Justin Time - Reinstate Monica
显示剩余4条评论

29

除了其他答案之外。

内联命名空间可用于在符号中编码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
};
}
现在,如果您查看使用发布标志构建的每个对象的对象文件以及使用调试标志构建的其他对象的对象文件,则会发现链接器符号也包括内联命名空间名称。在这种情况下。
$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar

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

请注意符号名称中存在reldbg

现在,如果您尝试将调试模式与发布模式或反之链接,您将获得一个与运行时错误相反的链接器错误。


1
是的,这很有道理。所以这更适用于库实现者等人。 - Walter

10
综上所述,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> {};
}

这将显示一个静态分析警告:在命名空间'A'外,类模板的第一个专门化声明是C++11扩展[-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 命名空间的另一个功能是允许库编写者提供对库的透明更新,而不需要强制用户重构使用新命名空间名称的代码,并且防止缺乏冗余度并提供 API 不相关细节的抽象,同时 4) 提供与使用非内联命名空间相同的有益链接器诊断和行为。假设您正在使用一个库:

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

它允许用户在不需要知道或包含文档中的ABI版本的情况下调用library :: foo,看起来更加简洁。使用library :: abiverison129389123 :: foo会显得混乱。
当对foo进行更新时(例如向类中添加新成员),它不会影响API级别上的现有程序,因为它们不会使用该成员,而且内联命名空间名称的更改也不会在API级别上更改任何内容,因为library :: foo仍将起作用。
namespace library {
    inline namespace abi_v2 {
        class foo {
            //new member
        } 
    }
}

然而,对于与之链接的程序来说,由于内联命名空间名称被编译成类似常规命名空间的符号名称,因此更改对于链接器来说并不是透明的。因此,如果应用程序没有重新编译但却链接了新版本的库,它将会出现一个找不到符号abi_v1的错误,而不是在运行时由于ABI不兼容导致神秘的逻辑错误。即使添加一个新成员在编译时不影响程序(API级别),由于类型定义的更改,也会导致ABI兼容性问题。
在这种情况下:
namespace library {
    namespace abi_v1 {
        class foo {
        } 
    }

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

使用两个非内联命名空间可以使库的新版本链接而无需重新编译应用程序,因为 abi_v1 将被编码为其中一个全局符号,并且它将使用正确(旧)的类型定义。但重新编译应用程序将导致引用解析为library::abi_v2

使用 using namespace 不如使用 inline 功能强大(因为无法解决外部定义),但提供了与上述相同的 4 个优点。但真正的问题是,为什么继续使用变通方法,现在有专门的关键字来完成它呢。这是更好的实践,更少的冗余(只需要更改 1 行代码而不是 2 行),并且使意图明确。


6

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

使用 Qt,您可以使用 Q_ENUM_NS 获得一些额外的好功能,这又要求封闭命名空间具有元对象,并使用 Q_NAMESPACE 声明。但是,为了使 Q_ENUM_NS 正常工作,必须在同一文件中存在相应的 Q_NAMESPACE⁽¹⁾。否则,就会出现重复定义错误。这实际上意味着所有枚举值都必须在同一个头文件中。糟糕。

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

因此,如果出于某种原因需要将内容拆分为多个子命名空间,并且所有子命名空间都“看起来”像一个命名空间,则它们非常有用。当然,这类似于在外部命名空间中编写 using namespace inner,但避免了两次编写内部命名空间名称导致DRY违规的问题。


  1. 事实上还更糟糕,它必须在同一组大括号中。

  2. 除非您尝试访问元对象而不完全限定它,但元对象很少直接使用。


1
你能用代码框架勾勒一下吗?最好不要明确提到Qt。这听起来相当复杂/不清楚。 - Walter
2
不是很容易。需要使用单独的命名空间的原因与Qt实现细节有关。说实话,很难想象在Qt之外会有同样要求的情况。然而,在这种特定于Qt的场景中,它们非常有用!请参见https://gist.github.com/mwoehlke-kitware/bc790dcd474f4b34b812fb34d6a9c8b0或https://github.com/Kitware/seal-tk/pull/45以获取示例。 - Matthew

6

内联命名空间也可以用来在命名空间中提供更细粒度的特性/名称访问。

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

  • 如果您在某个地方使用using namespace std;,则还可以访问std中所有用户定义的字面值。
  • 但是,如果您只需要一组udl在本地代码中,您还可以执行using namespace std::literals::string_literals;,然后您将仅获取在该命名空间中定义的udl符号。

对于您想要无限制访问的符号(udl、运算符等),这似乎是一种有用的技术,在其中您可以将它们打包成一个内联命名空间,以便您可以针对整个库的命名空间执行特定的使用,而不是整个库的命名空间。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接