命名空间、using声明和using指示【附送彩蛋】

时间:2022-09-08 09:17:10

用过Objective-C的程序员都有过这样的体验,Objective-C的一些函数名很长,有些长的甚至不可思议,某些方法的名字带参数甚至超过一行!命名空间、using声明和using指示【附送彩蛋】

可能原因之一:Objective-C没有命名空间,所以只能靠程序员不停地加前缀了。这全当是开篇小幽默吧。


引入文章的第一个主角:命名空间

传统上,程序员通过将其定义的全局实体名字设得很长(也就是加很多前缀),以避免命名空间污染的问题。对于程序员来说,书写和阅读这么长的名字费时费力且过于烦琐。命名空间由此产生。通过namespace,可以避免全局名字固有的限制。


namespace的定义:(注意 namespace不能定义在函数或类的内部

namespace my_First{
	class Query{/*类的定义及接口部分*/};
	class Query_basse{/*类的定义及接口部分*/};
} //命名空间结束后无须分号,这一点与块类似


虽然namespace具有不连续性,可以定义在几个不同的地方,但是,对于具有同一个名字的namespace来说,其中的名字也要在该namespace中保持唯一。


Tip: 关于namespace中写在.h文件还是.cpp文件

  • 命名空间的一部分成员的作用是定义类,以及声明作为类接口的函数及对象,则这些成员应该置于.h头文件中,这些头文件将被包含在作用了这些成员的文件中。
  • 命名空间成员的定义部分则置于另外的源文件中。定义多个类型不相关的命名空间,应该使用单独的文件分别表示每个类型。

inline namespace(内联命名空间 C++11 新特性)

顾名思义,内联即为在定义的地方进行展开。所以内联命名空间中的名字可以被外层命名空间直接使用。同时我们无须在内联命名空间的名字前添加表示该命名空间的前缀,就可以通过外层命名空间的名字,直接使用它。定义内联命名空间的方式是在关键字namespace前添加关键字inline 。
inline namespace my_Inline{
<span style="white-space:pre">	</span>//成员
}
inline namespace最主要的使用地方是,在应用程序的新版本发布后,我们可以将新版本的所有代码头文件入在inline namespace中,而之前版本的代码都放在一个非内联命名空间中。
//SecondEdition.h
inline namespace SecondEdition{
	//第二版代码头文件
}

//FirstEdition.h
namespace FirstEdition{
	//第一版代码头文件
}

namespace my_Project{
	#include "FirstEdition.h"
	#include "SecondEdition.h"
}
因为SecondEdition是内联的,所以形如 my_Project::的代码可以直接使用新版本SecondEdition的成员。如果想要使用早期版本的代码,则必须像其他嵌套的命名空间一样加上完整的外层命名空间名字,即my_Project::FirstEdition::这种形式。

区别 全局命名空间和匿名的命名空间

全局作用域中定义的名字被隐式地加入到全局命名空间中。因为全局作用域是隐式地,所以它并没有名字。但它不是匿名的命名空间。对于全局命名空间,可以通过::来进行访问。
::member_name

对于匿名的命名空间(关键字namespace后紧跟着花括号括起来的一系列声明语句),其作用域仅限于定义它的文件内。在定义它的文件中,未命名空间中的名字可以直接使用,就像是使用本文件的静态变量一样。也就是说,匿名空间中的成员相当于本文件的static成员。
现在在文件中进行静态声明的做法已经被C++标准取消了,现在正确的做法是使用未命名的命名空间。

命名空间别名

对于一个很长名字的命名空间,可以给该空间的名字起一个别名(namespace alias),例如:
namespace mP = my_Project;

命名空间的别名也可以指向一个嵌套的命名空间。一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。

using声明或using指示

命名空间除了直接使用::访问符来引用之外,还可以通过using声明或using指示来使用。

using声明

using声明引入的名字遵守与过去一样的作用域规则:它的有效范围从using声明的地方开始,一直到using声明所在的作用域结束为止。在此过程中,外层作用域的同名实体将被隐藏。在类的作用域中,using声明语句只能指向基类成员。
class Base{
public:
	std::size_t size() const{return n;}
protected:
	std::size_t n;
};
//private继承关闭了派生类用户使用基类public成员的通道,同时关闭了 孙类 继承 爷爷类 public和protected成员的通道。
class Derived : private Base{
public:
	using Base::size;
protected:
	using Base::n;
};

using声明函数

namespace my_Test{
	void showMe() { cout << "my_Test::void_showMe" << endl; }
	void showMe(int) { cout << "my_Test::void_showMe_Int" << endl; }
	void showMe(double) { cout << "my_Test::void_showMe_Double" << endl; }
}
//using声明:
using my_Test::showMe;

我们可以看到,声明使用的是这个函数的名字,不包括函数返回类型及参数列表等,仅仅是一个名字。当我们为函数书写using声明时,但具有这个名称的所有的函数都被引入这个作用域中。
</pre><span style="white-space:pre"> </span>一个using声明函数,囊括了重载函数的所有版本以确保不违反命名空间的接口。库的作者为某项任务提供了好几个不同的函数,允许用户选择性地忽略重载函数中的一部分但不是全部,有可能会导致意想不到的程序行为。所以要保证该命名空间内的所有同名函数均被引入进来。</div><div><span style="white-space:pre"> </span>如果using声明出现在局部作用域中,则引入的名字将隐藏外层作用域的相关声明。如果using声明所在的作用域内已经有一个函数与新引入的函数同名且形参相同,则该using声明将引发错误。但是如是该作用域内的函数同名但是形参列表不一样,则也将会把该函数加入到重载集合中。请结合下面这个例子体会之。</div>
namespace libs_R_us{
	extern void print(int);
	extern void print(double);
}
//普通声明
void print(const std::string &);

//这个using指示把名字添加到print调用的候选函数集中
using namespace libs_R_us;

//此时,print调用的候选重载函数包括:
//libs_R_us中的void print(int);
//libs_R_us中的void print(double);
//显式声明的void print(const std::string &);

void fooBar(int iVal){
	print("Hello");			//调用全局函数void print(const std::string &);
	print(iVal);			//调用libs_R_us中的void print(int);
}

而对于using指示来说,引入一个与已有函数形参列表完全相同的函数并不会产生错误。此时,只要我们在使用的地方指明调用的是命名空间中的函数版本还是作用域的版本即可。接下来就开始介绍using指示。

using指示

namespace my_Test{
	void showMe() { cout << "my_Test::void_showMe" << endl; }
	void showMe(int) { cout << "my_Test::void_showMe_Int" << endl; }
	void showMe(double) { cout << "my_Test::void_showMe_Double" << endl; }
}
using namespace my_Test;

可以看到,using指示把该命名空间的开口一次彻底打开了。using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是,但是, 不能出现在类的作用域中。书上说的 “ using指示一次性注入某个命名空间的所有名字”,这样说其实不准确。准确地说法应该是,using指示一次性的打开了某个命名空间的大门,指示编译器以后在使用的地方,除了在原来的地方查找之外,还要进入该命名空间查找。这也正是using指示中 “指示”二字的由来。

using声明与using指示的区别

using声明的名字的作用域和using声明语句本身的作用域一致,从效果上看,就好像using声明语句为命名空间的成员在当前作用域内创建了一个别名一样。
但是,对于using指示,它只是告诉编译器,在本作用域内,在使用成员的地方,编译时要查找该成员的类型定义,如果使用的地方没有显式指明成员所属的地方,那么,除了在本作用域内找之外,同时一定还要在该命名空间中找,如果都找到了,就要报错。因为冲突产生了。想要解决冲突的话,可以在使用成员的地方显式指明使用的是哪个地方的成员。
一般来说,使用using声明总是对的,使用using指示总是会带来风险的。using指示引发的二义性错误只有在使用了冲突的名字的地方才能被发现。这种延后的检测意味着可能在特定库引入很久很久之后,才爆发冲突。using声明不是不会引起冲突,而是,由using声明引起的二义性问题在声明处就会被发现,无须等到使用名字的地方,这显然对检测并修正错误大有益处。但using指示并不是一无是处,例如在命名空间本身的实现文件中,就可以使用using指示。

using彩蛋

首先我们来看一个大家都习以为常的小程序,以此来引入我们的彩蛋。
#include <string>
#include <iostream>

using std::cout;

int main(){
	std::string myStr = "Hello";
	cout << myStr;
	
	return 0;
}

正如我们所知,该输出命令等价于
 operator<<(std::cout,myStr);
operator<<函数是定义在标准库string中,string定义在命名空间std中。但是我们没有使用std::限定符或者using声明就可以直接调用operator<<     这是为什么呢?

argument-dependent lookup或Koening lookup法则

彩蛋来了,这是因为有这样一条规则存在:当我们给函数传递一个类类型的对象时,首先会在常规的作用域查找,其次在实参类所属的命名空间查找。
查找顺序如下:
1. 先在本作用域内查找;
2. 在实参的命名空间 和 全局作用域中 同时查找;

这一规则也叫做argument-dependent lookup或Koening lookup法则。这一规则对传递类的引用或指针的调用同样有效。如果名称后面的括号里面有一个或多个实参表达式,那么ADL将会查找这些实参直接相关的所有namespace和class。其中,类包括直接基类和间接基类。Andrew Koenig首次提出了ADL,这也是为什么ADL有时也称为koenig查找的原因。最初ADL引入的动机只是为了方便编写a+b,其中a和b至少有一个是用户自定义类型。如果没有ADL,则就要写为N::operator+(a,b)。

针对上述规则,我们使用以下程序进行验证:
提示:对于一个未声明的类或函数 以友元的身份,第一次出现在友元声明中,则我们认为它是最近的外层命名空间的成员。这条规则与上述规则一起,会产生意想不到的效果。

namespace A{
	class C{
		friend void f2();			//除非另有声明,否则不会被找到
		friend void f(const C&);	//根据实参相关的查找,就可以被找到
	};

}

int main(){
	A::C cobj;
	f(cobj);	//正确,通过在A::C中的友元声明找到A::f
	f2();		//错误,A::f2没有被声明
}

因为f接受了一个类类型的实参,而且f在C所属的命名空间进行了隐式的声明,所以f能被找到。相反,因为f2没有类类型的形参,所以它无法被找到。