提问人:Walter 提问时间:6/13/2012 最后编辑:Marc Mutz - mmutzWalter 更新时间:2/2/2022 访问量:79405
内联命名空间有什么用?
What are inline namespaces for?
问:
C++ 11 允许 s,其所有成员也自动在封闭 中。我想不出任何有用的应用——有人能举一个简短、简洁的例子来说明需要 an 的情况以及它是最惯用的解决方案吗?inline namespace
namespace
inline namespace
(另外,我不清楚在一个但不是所有声明中声明 a 时会发生什么,这些声明可能存在于不同的文件中。这不是自找麻烦吗?namespace
inline
答:
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
评论
f(1)
V99
using namespace Mine;
Mine
Mine::V99
inline
V99.h
V100.h
Mine.h
Mine.h
V100.h
Mine.h
V98.h
V99.h
Mine.h
V98.h
V99.h
V100.h
Mine::V98::f
Mine::V98::f
Mine::f
Mine
Mine::V99
Mine::V98
内联命名空间是一种类似于符号版本控制的库版本控制功能,但纯粹在 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 版本在升级编译器时给您带来了麻烦,那么您所要做的“所有”就是在代码库中找到对 的引用并将它们替换为 .__cplusplus
vector
vector
std::vector
std::pre_cxx_1997::vector
到了下一个标准,STL 供应商只是再次重复该过程,为 with support(需要 C++11)引入一个新的命名空间并内联该命名空间 iff .std::vector
emplace_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 中的实现更有效的实现。
但是:在专用模板时,您需要在声明模板的命名空间中执行此操作。标准说这是在命名空间中声明的,所以这是用户理所当然地期望专门化类型的地方。vector
std
此代码适用于非版本控制命名空间 ,或 C++11 内联命名空间功能,但不适用于使用的版本控制技巧,因为这会公开定义的真正命名空间不是直接的实现细节。std
using namespace <nested>
vector
std
还有其他漏洞可以检测嵌套命名空间(请参阅下面的注释),但内联命名空间会将它们全部堵塞。仅此而已。对未来非常有用,但 AFAIK 标准没有为其自己的标准库规定内联命名空间名称(不过,我很想被证明是错误的),因此它只能用于第三方库,而不能用于标准本身(除非编译器供应商同意命名方案)。
评论
using namespace V99;
std::cxx_11
using namespace A
B::name
ifdef
using
除了所有其他答案。
内联命名空间可用于对符号中函数的 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
};
}
现在,如果您查看每个对象的目标文件,则使用release构建一个对象,另一个使用debug标志构建一个对象。您会发现链接器符号也包含内联命名空间名称。在这种情况下
$ nm rel.o
T__ ZROKfoo9relEbar
$ nm dbg.o
T__ ZROKfoo9dbgEbar
链接器符号名称可能不同。
请注意符号名称中存在 和 。rel
dbg
现在,如果您尝试将调试与发布模式链接,反之亦然,您将收到与运行时错误相反的链接器错误。
评论
我实际上发现了内联命名空间的另一种用途。
使用Qt,你可以使用Q_ENUM_NS
获得一些额外的、不错的功能,这反过来又要求封闭的命名空间有一个元对象,该元对象是用 .但是,为了工作,同一文件中必须有一个对应的文件⁽¹⁾。而且只能有一个,否则你会得到重复的定义错误。实际上,这意味着所有枚举都必须位于同一标头中。呸。Q_NAMESPACE
Q_ENUM_NS
Q_NAMESPACE
或。。。您可以使用内联命名空间。在 中隐藏枚举会导致元对象具有不同的错误名称,而对用户来说,就像其他命名空间不存在⁽²⁾一样。inline namespace
因此,如果您出于某种原因需要这样做,它们对于将内容拆分为多个子命名空间非常有用,这些子命名空间看起来都像一个命名空间。当然,这类似于在外部命名空间中写入,但没有两次写入内部命名空间名称的 DRY 冲突。using namespace inner
实际上比这更糟糕;它必须在同一组大括号中。
除非您尝试在不完全限定元对象的情况下访问元对象,否则元对象几乎从未被直接使用过。
评论
因此,总结要点,并且并不相同,前者是在 C++11 中引入专用关键字(内联)之前版本库的解决方法,它修复了使用 的问题,同时提供相同的版本控制功能。如果在真正的命名空间之外完成,则使用用于导致 ADL 问题(尽管 ADL 现在似乎遵循指令),并且用户对库类/函数等进行行外专用化将不起作用(用户不会也不应该知道其名称,即用户将不得不使用 B::abi_v2:: 而不仅仅是 B:: 以解决专业化问题)。using namespace v99
inline namespace
using
using namespace
using
//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::foo
library::abiverison129389123::foo
当对 进行更新时,即向类添加新成员,它不会影响 API 级别的现有程序,因为它们不会使用该成员,并且内联命名空间名称的更改不会更改 API 级别的任何内容,因为仍然有效。foo
library::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_v1
library::abi_v2
using 的功能不如 using (因为行外定义无法解析),但提供了与上述相同的 4 个优点。但真正的问题是,当现在有专门的关键字来执行此操作时,为什么还要继续使用解决方法。这是更好的做法,不那么冗长(必须更改 1 行代码而不是 2 行代码),并使意图明确。using namespace
inline
内联命名空间还可用于提供对命名空间中功能/名称的细粒度访问。
这在 std::literals
中使用。std 中的命名空间都是内联命名空间,因此:literals
- 如果您在某处使用,您还可以访问 STD 中所有用户定义的文字。
using namespace std;
- 但是,如果您只需要本地代码中的一组 UDL,您也可以这样做,并且您将获得在该命名空间中定义的 UDL 符号。
using namespace std::literals::string_literals;
对于您想要访问非限定符号(udl、运算符等)的符号来说,这似乎是一种有用的技术,您可以在其中将它们捆绑在一个内联命名空间中,以便您可以只对该(子)命名空间而不是整个库的命名空间进行特定的使用。
评论