博客 阅读时间 5 分钟

在 C++ 项目中使用命名空间的 7 个常见原因

分享本文
7 Common Reasons for Using Namespaces in a C++ Project.

命名空间于 1995 年被引入 C++ 标准,通常被这样定义:

命名空间定义了一个新的作用域。命名空间提供了一种避免命名冲突的方式。

尽管命名空间在新近的 C++ 代码中被广泛使用,但大多数较老的代码并没有使用这一机制。

在研究了许多 C++ 项目的源代码之后,下面总结了这些项目中使用命名空间的一些常见原因。

1. 避免命名冲突。

在这种情况下,命名空间的使用主要对编译器有益;就代码的可读性或可维护性而言,它并没有为开发者提供额外的价值。

2. 对代码进行模块化

现代 C++ 库大量使用命名空间来模块化其代码库,并采用“按特性划分命名空间”(Namespace-by-feature)的方式。按特性划分命名空间,就是用命名空间来反映功能集:把与单个特性相关的所有条目(且仅与该特性相关的条目)放入同一个命名空间。这样得到的命名空间具有高内聚性和高模块性,命名空间之间的耦合也最小。紧密协作的条目被放在一起。

Boost 是按特性分组的绝佳范例;它包含数千个命名空间,每个命名空间用于组织一个特定的特性。

3. 匿名命名空间。

无名命名空间避免了对全局 static 变量的需求。您创建的匿名命名空间只在定义它的文件内可访问。

4. 解决枚举(enum)问题的变通方法。

C++ 中的“传统”枚举会把它们的枚举值导出到外围作用域,如果同一作用域中的两个不同枚举定义了同名的枚举值,就可能导致命名冲突。

在大型项目中,无法保证两个不同的枚举不会使用相同的名字。这个问题在 C++11 中通过enum class得到了解决,它隐式地把枚举值限定在枚举名的作用域内。

在 C++11 之前,一个常见的变通方法是把枚举声明在命名空间内。例如,不要像这样声明枚举:

enum status{
  status_ok,
  status_error
};

而是可以把它声明在一个命名空间内:

namespace status{
       enum status{
         ok,
          error
  };
}

许多 C++ 项目都使用这个技巧;例如,虚幻引擎(Unreal Engine)的源代码就广泛使用了这种技术。

5. 按约定隐藏细节

对于代码在头文件中实现的模板库,向库的使用者表明某些类型不应直接使用(因为它们是实现细节)是很有用的。在 C# 中,“internal” 关键字可以胜任这个目的,但在 C++ 中没有办法对库的使用者隐藏公有类型。

现代 C++ 中有一个由 Boost 库的开发者开创的常见惯用法:把那些属于模块实现(即不属于公共 API)但又必须公开可用的符号,隔离到一个单独的子命名空间中——按惯例命名为 detail。

例如,Boost.Math 的文档明确指出:

不打算供应用程序使用的函数位于boost::math::detail的关联命名空间。

C++11 引入的内联命名空间提供了两种额外的可能:

当定义一个内联命名空间时,一条using指令会被隐式插入其外层命名空间。在通过外层命名空间查找限定名时,内联命名空间的成员会被隐式的using指令引入并被找到,即使该名字是在外层命名空间中声明的。

例如,如果在定义了 USE_INLINE_B 的情况下编译以下代码,生成的可执行文件输出 1;否则输出 2。

namespace A {
#if USE_INLINE_B
   inline
#endif
   namespace B {
      int foo(bool) { return 1; }
   }
   int foo(int) { return 2; }
}

int main(void) {
   return A::foo(true);
}

下面是内联命名空间的另外两种用途,如这份文档的关联命名空间。

6. 在显式实例化和特化中使用内联命名空间定义

您可以显式实例化或特化内联命名空间的每个成员,就像它是其外层命名空间的成员一样。例如,在某个命名空间(如 M)中,对显式实例化或特化的主模板进行名称查找时,会考虑其外层命名空间集合包含 M 的那些内联命名空间。例如:

namespace L {
   inline namespace M {
       template <typename T> class C; 
   }

   template <typename T> void f(T) { /*...*/ };
}

struct X { /*...*/ };
namespace L {
  template<> class C<X> { /*...*/ };    //template specialization
}

int main()
{
  L::C<X> r;
  f(r);  // fine, L is an associated namespace of C
}

在这个例子中,

M

是其外层命名空间

L

的内联命名空间;类

C

是内联命名空间

M

的成员,因此

L

是类

C

的关联命名空间。

在显式实例化和特化中使用内联命名空间定义时,适用以下规则:

  • 如果模板名是限定的,显式实例化必须位于主模板的某个外层命名空间中;否则,它必须位于主模板最近的外层命名空间,或其外层命名空间集合中的某个命名空间。
  • 显式特化声明必须先在主模板最近外层命名空间的命名空间作用域中声明,或在其外层命名空间集合中的某个命名空间中声明。如果该声明不是定义,则之后可以在任何外层命名空间中定义。

7. 在库版本管理中使用内联命名空间定义

借助内联命名空间定义,您可以为一个拥有多个实现的库提供统一的源代码接口,库的使用者可以选择其中一个实现与该公共接口关联。下面的例子演示了在库版本管理中使用内联命名空间与显式特化的方法。

//foo.h
#ifndef SOME_LIBRARY_FOO_H_
   #define SOME_LIBRARY_FOO_H_
   namespace SomeLibrary
   {
      #ifdef SOME_LIBRARY_USE_VERSION_2_
         inline namespace version_2 { }
      #else    
      	  inline namespace version_1 { }
      #endif
      namespace version_1 {
       	 template <typename T> int foo(T a) {return 1;}
      }     
      namespace version_2 {
       	 template <typename T> int foo(T a) {return 2;}
      }
   }  
#endif 

//myFooCaller.C 
#include <Foo.h>
#include <iostream>

struct MyIntWrapper { int x;}; 

//Specialize SomeLibrary::foo()
//Should specialize the correct version of foo()

namespace SomeLibrary {
	  template <> int foo(MyIntWrapper a) { return a.x;}
}

int main(void) {
	  using namespace SomeLibrary;
	  MyIntWrapper intWrap = { 4 };
	  std::cout << foo(intWrap) + foo(1.0) << std::endl;
}

如果在定义了 SOME_LIBRARY_USE_VERSION_2_ 的情况下编译这个例子,生成的可执行文件输出 6;否则输出 5。如果函数调用

foo(intWrap)

用其中一个内联命名空间加以限定,那么您需要确保显式特化是有效的。

分享本文