C++20标准下的嵌套命名空间和内联命名空间

C++20标准下的嵌套命名空间和内联命名空间

一、命名空间概述

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

例如:

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函数需要使用::来指定函数的命名空间。

一、嵌套命名空间

嵌套的命名空间是指定义在其他命名空间中的命名空间:

namespace S
{
    // 第一个嵌套的命名空间,定义了class A
    namespace S1
    {
        class A {};
    }
    
    // 第二个嵌套的命名空间,定义了class B
    namespace S2
    {
        class B {};
    }
}

嵌套的命名空间同时是一个嵌套的作用域,它嵌套在外层命名空间的作用域中。嵌套的命名空间中的名字遵循的规则与往常类似:内层命名空间声明的名字将隐藏外层命名空间声明的同名成员。在嵌套的命名空间中定义的名字只在内层命名空间中有效,外层命名空间中的代码想要访问它必须在名字前面添加限定符。例如:

S::S1::A

C++ 17标准中允许使用一种更简洁的形式描述嵌套命名空间,例如:

namespace A::B::C
{
    int foo() { return 0; }
}

以上代码完全等价于:

namespace A
{
    namespace B
    {
        namespace C
        {
            int foo() { return 0; }
        }
    }
}

很显然前者是一种更简洁的定义嵌套命名空间的方法。使用新的嵌套命名空间语法能够有效消除代码冗余,提高代码的可读性。

二、内联命名空间

C++11 标准中引入了一种新的嵌套命名空间,称为内联命名空间(inline namespace)。和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就是说,我们无须在内联命名空间的名字前添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问它。

定义内联命名空间的方式是在关键字 namespace 前添加关键字 inline。

#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();
}

关键字 inline 必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。

这个新特性的用途是什么呢?这里删除内联命名空间,将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函数。

有些遗憾的是,在C++17标准中没有办法简洁地定义内联命名空间,这个问题直到C++20标准才得以解决。在C++20中,我们可以这样定义内联命名空间:

namespace A::B::inline C
{
    int foo() { return 5; }
}

// 或者
namespace A::inline B::C
{
    int foo() { return 5; }
}

它们分别等同于:

namespace A::B
{
    inline namespace C
    {
        int foo() { return 5; }
    }
}

namespace A
{
    inline namespace B
    {
        namespace C
        {
            int foo() { return 5; }
        }
    }
}

请注意,inline可以出现在除第一个namespace之外的任意namespace之前。


参考文献:

  1. 《C++ Primer第五版》
  2. 《现代C++语言核心特性解析》

猜你喜欢

转载自blog.csdn.net/hubing_hust/article/details/128604966
今日推荐