第2章 内联和嵌套命名空间(C++11~C++20)
2.1 内联命名空间的定义和使用
开发一个大型工程必然会有很多开发人员的参与,也会引入很多第三方库,这导致程序中偶尔会碰到同名函数和类型,造成编译冲突的问题。为了缓解该问题对开发的影响,我们需要合理使用命名空间。程序员可以将函数和类型纳入命名空间中,这样在不同命名空间的函数和类型就不会产生冲突,当要使用它们的时候只需打开其指定的命名空间即可,例如:
namespace S1 {
void foo() {}
}
namespace S2 {
void foo() {}
}
using namespace S1;
int main()
{
foo();
S2::foo();
}
以上是命名空间的一个典型例子,例子中命名空间S1
和S2
都有相同的函数foo
,在调用两个函数时,由于命名空间S1
被using
关键字打开,因此S1
的foo
函数可以直接使用,而S2
的foo
函数需要使用::来指定函数的命名空间。
C++11标准增强了命名空间的特性,提出了内联命名空间的概念。内联命名空间能够把空间内函数和类型导出到父命名空间中,这样即使不指定子命名空间也可以使用其空间内的函数和类型了,比如:
#include <iostream>
namespace Parent {
namespace Child1
{
void foo() { std::cout << "Child1::foo()" << std::endl; }
}
inline namespace Child2
{
void foo() { std::cout << "Child2::foo()" << std::endl; }
}
}
int main()
{
Parent::Child1::foo();
Parent::foo();
}
在上面的代码中,Child1
不是一个内联命名空间,所以调用Child1
的foo
函数需要明确指定所属命名空间。而调用Child2
的foo
函数则方便了许多,直接指定父命名空间即可。现在问题来了,这个新特性的用途是什么呢?这里删除内联命名空间,将foo
函数直接纳入Parent
命名空间也能达到同样的效果。
实际上,该特性可以帮助库作者无缝升级库代码,让客户不用修改任何代码也能够自由选择新老库代码。举个例子:
#include <iostream>
namespace Parent {
void foo() { std::cout << "foo v1.0" << std::endl; }
}
int main()
{
Parent::foo();
}
假设现在Parent
代码库提供了一个接口foo
来完成一些工作,突然某天由于加入了新特性,需要升级接口。有些用户喜欢新的特性但并不愿意为了新接口去修改他们的代码;还有部分用户认为新接口影响了稳定性,所以希望沿用老的接口。这里最直接的办法是提供两个不同的接口函数来对应不同的版本。但是如果库中函数很多,则会出现大量需要修改的地方。另一个方案就是使用内联命名空间,将不同版本的接口归纳到不同的命名空间中,然后给它们一个容易辨识的空间名称,最后将当前最新版本的接口以内联的方式导出到父命名空间中,比如:
namespace Parent {
namespace V1 {
void foo() { std::cout << "foo v1.0" << std::endl; }
}
inline namespace V2 {
void foo() { std::cout << "foo v2.0" << std::endl; }
}
}
int main()
{
Parent::foo();
}
从上面的代码可以看出,虽然foo
函数从V1
升级到了V2
,但是客户的代码并不需要任何修改。如果用户还想使用V1
版本的函数,则只需要统一添加函数版本的命名空间,比如Parent::V1::foo()
。使用这种方式管理接口版本非常清晰,如果想加入V3
版本的接口,则只需要创建V3
的内联命名空间,并且将命名空间V2
的inline
关键字删除。请注意,示例代码中只能有一个内联命名空间,否则编译时会造成二义性问题,编译器不知道使用哪个内联命名空间的foo
函数。