3.9 内联名字空间
类别:部分人
在老式的C语言编程的实际项目中,我们常会需要一个“字典”来记录程序中所有的名字。这是由于C中所有的非静态全局变量、非静态的函数名都是都是全局共享的。那么对于多个程序员合作编程而言,总是需要知道自己给变量函数取的名字是否冲突,以避免发生编译错误,因此字典是一种使用C语言合作编程的一种重要交流手段。
在C++中,引入了名字空间(namespace)这样一个概念。名字空间的目的是分割全局共享的名字空间。程序员在编写程序时可以建立自己的名字空间,而使用者则可以通过双冒号“空间名::函数/变量名”的形式来引用自己需要的版本。这就解决了C中名字冲突的问题。不过有很多时候,我们会遇到一个名字空间下包含多个子名字空间的状况。子名字空间通常会带来一些使用上的不便。我们来看看代码清单3-47所示的这个例子。
代码清单3-47
include <iostream>
using namespace std;
//这是Jim编写的库,用了Jim这个名字空间
namespace Jim{
namespace Basic{
struct Knife{Knife(){cout<<"Knife in Basic."<<endl;}};
class CorkScrew{};
}
namespace Toolkit{
template<typename T>class SwissArmyKnife{};
}
//…
namespace Other{
Knife b;//无法通过编译
struct Knife{Knife(){cout<<"Knife in Other"<<endl;}};
Knife c;//Knife in Other
Basic::Knife k;//Knife in Basic
}
}
//这是LiLei在使用Jim的库
using namespace Jim;
int main(){
Toolkit::SwissArmyKnife<Basic::Knife>sknife;
}
//编译选项:g++3-9-1.cpp
在代码清单3-47中,库的编写者Jim用名字空间将自己的代码封装起来。同时,该程序员把名字空间继续细分为Basic、Toolkit及Other等几个。可以看到,通过名字空间的细分,Other名字空间中不能直接引用Basic名字空间中的名字Knife。而Other名字空间中定义了Knife类型,那么变量c的声明就会导致其使用的Knife类型是属于名字空间Other中的版本的。这样的使用名字空间的方式是非常清楚的。
不过Jim这样会带来一个问题,即库的使用者在使用Jim名字空间的时候,需要知道太多的子名字空间的名字。使用者显然不希望声明一个sknife变量时,需要Toolkit::SwissArm yKnife<Basic::Knife>这么长的类型声明。而从库的提供者Jim的角度看,通常也没必要让使用者LiLei看到子名字空间,因此他可能考虑这样修改代码,如代码清单3-48所示。
代码清单3-48
include <iostream>
using namespace std;
namespace Jim{
namespace Basic{
struct Knife{Knife(){cout<<"Knife in Basic."<<endl;}};
class CorkScrew{};
}
namespace Toolkit{
template<typename T>class SwissArmyKnife{};
}
//…
namespace Other{
//…
}
//打开一些内部名字空间
using namespace Basic;
using namespace Toolkit;
}
//LiLei决定对该class进行特化
namespace Jim{
template<>class SwissArmyKnife<Knife>{};//编译失败
}
using namespace Jim;
int main(){
SwissArmyKnife<Knife>sknife;
}
//编译选项:g++3-9-2.cpp
在代码清单3-48所示的例子中,Jim在名字空间Jim的最后部分,打开了(using)Basic和Toolkit两个名字空间(我们省略了关于Other名字空间中的部分)。这样一来在代码清单3-48中遇到的名字过长的问题就不复存在了。不过这里又有了新的问题:库的使用者LiLei由于觉得Toolkit中的模板SwissArmyKnife有的时候不太合用,所以决定特化一个SwissArmyKnife<Knife>的版本。这个时候,我们编译该例子则会失败。这是由于C++98标准不允许在不同的名字空间中对模板进行特化造成的。
在C++11中,标准引入了一个新特性,叫做“内联的名字空间”。通过关键字“inline namespace”就可以声明一个内联的名字空间。内联的名字空间允许程序员在父名字空间定义或特化子名字空间的模板。我们可以看看代码清单3-49所示的例子。
代码清单3-49
include <iostream>
using namespace std;
namespace Jim{
inline namespace Basic{
struct Knife{Knife(){cout<<"Knife in Basic."<<endl;}};
class CorkScrew{};
}
inline namespace Toolkit{
template<typename T>class SwissArmyKnife{};
}
//…
namespace Other{
Knife b;//Knife in Basic
struct Knife{Knife(){cout<<"Knife in Other"<<endl;}};
Knife c;//Knife in Other
Basic::Knife k;//Knife in Basic
}
}
//这是LiLei在使用Jim的库
namespace Jim{
template<>class SwissArmyKnife<Knife>{};//编译通过
}
using namespace Jim;
int main(){
SwissArmyKnife<Knife>sknife;
}
//编译选项:g++ -std=c++11 3-9-3.cpp
代码清单3-49中,我们将名字空间Basic和Toolkit都声明为inline的。此时,LiLei对库中模板的偏特化(SwissArmyKnife<Knife>)则可以通过编译。不过这里我们需要再次注意一下Other这个名字空间中的状况。可以看到,变量b的声明语句是可以通过编译的,而且其被声明为一个Basic::Knife的类型。如果换个角度理解的话,在子名字空间Basic中的名字现在看起来就跟在父名字空间Jim中一样。了解了这一点,读者或者会皱起眉头,Jim名字空间中的良好分隔明显被破坏了,要做到这样的效果,只需要把Knife和CorkScrew放到全局名字空间中就可以了,根本不用inline namespace这么复杂。事实上,这跟inline namespace的使用方式有关。我们可以看看代码清单3-50所示的例子。
代码清单3-50
include <iostream>
using namespace std;
namespace Jim{
if__cplusplus==201103L
inline
endif
namespace cpp11{
struct Knife{Knife(){cout<<"Knife in c++11."<<endl;}};
//…
}
if__cplusplus<201103L
inline
endif
namespace oldcpp{
struct Knife{Knife(){cout<<"Knife in old c++."<<endl;}};
//…
}
}
using namespace Jim;
int main(){
Knife a;//Knife in c++11.(默认版本)
cpp11::Knife b;//Knife in c++11.(强制使用cpp11版本)
oldcpp::Knife c;//Knife in old c++.(强制使用oldcpp11版本)
}
//编译选项:g++ -std=c++11 3-9-4.cpp
在代码清单3-50中,Jim为它的名字空间设定了两个子名字空间:cpp11和oldcpp。这里我们看到了在2.1节中提到的关于C++的宏cplusplus。代码的意思是,如果现在的宏cplusplus等于201103这个常数,那么就将名字空间cpp11内联到Jim中,而如果小于201103,则将名字空间oldcpp内联到Jim中。这样一来,编译器就可以根据当前编译器对C++支持的状况,选择合适的实现版本。而如果需要的话,我们依然可以通过名字空间的方式(如cpp11::Knife)来访问相应名字空间中的类型、数据、函数等。这对程序库的发布很有好处,因为需要长期维护的程序库,可能版本间的接口和实现等都随着程序库的发展而发生了变化。那么根据需要将合适的名字空间导入到父名字空间中,无疑会方便库的使用。
事实上,在C++标准程序库中,开发者已经开始这么做了。如果程序员需要长期维护、发布不同库的版本,不妨试用一下内联名字空间这个特性。
还有一点需要指出的是,匿名的名字空间同样可以把其包含的名字导入父名字空间。所以读者可能认为代码清单3-50中的代码同样可以通过匿名名字空间与宏组合来实现。不过跟代码清单3-48中使用using打开名字空间的情况一样,匿名名字空间无法允许在父名字空间的模板特化。这也是C++11中为什么要引入新的内联名字空间的一个根本原因。不过与我们在代码清单3-49中看到的一样,名字空间的内联会破坏该名字空间本身具有的封装性,所以程序员不应该在需要隔离名字的时候使用inline namespace关键字。
此外,在代码实践时,读者可能还会被一些C++的语言特性迷惑,比较典型的是所谓“参数关联名称查找”,即ADL(Argument-Dependent name Lookup)。这个特性允许编译器在名字空间内找不到函数名称的时候,在参数的名字空间内查找函数名字。比如说下面这个例子:
namespace ns_adl{
struct A{};
void ADLFunc(A a){}//ADLFunc定义在namespace ns_adl中
}
int main(){
ns_adl::A a;
ADLFunc(a);//ADLFunc无需声明名字空间
}
函数ADLFunc就无需在使用时声明其所在的名字空间,因为编译器可以在其参数a的名字空间ns_adl中找到ADLFunc,编译也就不会报错了。
ADL带来了一些使用上的便利性,不过也在一定程度上破坏了namespace的封装性。很多人认为使用ADL会带来极大的负面影响[1]。因此,比较好的使用方式,还是在使用任何名字前打开名字空间,或者使用“::”列出变量、函数完整的名字空间。
[1]读者可以参考以下网页:http://stackoverflow.com/questions/2958648/what-are-the-pitfalls-of-adl