现代C++语言核心特性解析
上QQ阅读APP看书,第一时间看更新

第2章 内联和嵌套命名空间(C++11~C++20)

2.1 内联命名空间的定义和使用

开发一个大型工程必然会有很多开发人员的参与,也会引入很多第三方库,这导致程序中偶尔会碰到同名函数和类型,造成编译冲突的问题。为了缓解该问题对开发的影响,我们需要合理使用命名空间。程序员可以将函数和类型纳入命名空间中,这样在不同命名空间的函数和类型就不会产生冲突,当要使用它们的时候只需打开其指定的命名空间即可,例如:

namespace S1 {
  void foo() {}
}

namespace S2 {
  void foo() {}
}

using namespace S1;

int main()
{
  foo();
  S2::foo();
}

以上是命名空间的一个典型例子,例子中命名空间S1S2都有相同的函数foo,在调用两个函数时,由于命名空间S1using关键字打开,因此S1foo函数可以直接使用,而S2foo函数需要使用::来指定函数的命名空间。

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不是一个内联命名空间,所以调用Child1foo函数需要明确指定所属命名空间。而调用Child2foo函数则方便了许多,直接指定父命名空间即可。现在问题来了,这个新特性的用途是什么呢?这里删除内联命名空间,将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的内联命名空间,并且将命名空间V2inline关键字删除。请注意,示例代码中只能有一个内联命名空间,否则编译时会造成二义性问题,编译器不知道使用哪个内联命名空间的foo函数。