(点击上方公众号,可快速关注)
连接性(linkage)控制某个名字(可以是变量、函数、类等)在多个编译单元的共享,其在C++标准中的描述是异常复杂的。总体上,一个名字的连接性可以是以下三类中的一个:
外部连接(external linkage)
一个名字可以被其他编译单元使用,我们通常使用头文件引入的名字都具有外部连接,使得名字可以在不同的编译单元内使用。
内部连接(internal linkage)
一个名字可以在本编译单元内的任意代码使用,这是本篇要说的内容。
无连接(no linkage)
一个名字只能在某个代码块内使用,如常见的函数体,for、while代码块等。
内部连接在C中的表达
在C语言中,如果要使某个名字只在本编译单元内使用,而不暴露给其他编译单元,可以使用static
修饰该名字,如下示例代码:
// parse.h
#pragma once
#include <stdbool.h>
#include <stddef.h>
//检查协议的有效性
bool check_protocol_xx(const char* data, size_t len);
// parse.c
#include "parse.h"
// 检查协议头
static bool check_header(const char* data, size_t len) {
//省略处理代码
return true;
}
// 检查协议包体
static bool check_body(const char* data, size_t len) {
//省略处理代码
return true;
}
// 检查协议后缀
static bool check_suffix(const char* data, size_t len) {
//省略处理代码
return true;
}
bool check_protocol_xx(const char* data, size_t len) {
return check_header(data, len) && check_body(data, len) && check_suffix(data, len);
}
上面的例子模拟了一个检查协议xx格式的库,对外暴露的是check_protocol_xx
函数,其依赖的check_header
、check_body
和check_suffix
只是本编译单元使用,使用了static
限定。当然,这套机制可以在C++中使用,但实际上C++还引入了另一套机制实现这类功能,主要原因在于C中的这套机制仅能用于函数和变量,对于C++中的类等自定义类型无能为力。所以在C++中实现这类功能首选使用下面要介绍的匿名名字空间。
使用匿名名字空间表达内部连接
首先对上面的例子使用匿名名字空间进行改写,从而直观感受匿名名字空间的使用方式:
// parse.h
#pragma once
//检查协议的有效性
bool check_protocol_xx(const char* data, size_t len);
// parse.cpp
#include "parse.h"
namespace {
// 检查协议头
bool check_header(const char* data, size_t len) {
//省略处理代码
return true;
}
// 检查协议包体
bool check_body(const char* data, size_t len) {
//省略处理代码
return true;
}
// 检查协议后缀
bool check_suffix(const char* data, size_t len) {
//省略处理代码
return true;
}
}
bool check_protocol_xx(const char* data, size_t len) {
return check_header(data, len) && check_body(data, len) && check_suffix(data, len);
}
可以看到我们将之前的三个函数去掉static
限定后放在namespace
后的大括号内,转换过程比较直接。匿名名字空间跟普通的名字空间最大的区别就是namespace
后没有名字,故匿名名字空间也叫作不具名名字空间。这个例子与之前C的例子是完全等价的,网上不少文章的观点与之有差异,文章后面专门说明。
那么,匿名名字空间有名字吗?实际上是有的,只不过这个名字用户不知道,是由编译器负责生成,在本编译单元内唯一。由于用户不知道该名字,所以能保证不会被其他编译单元显式引用。C++17 标准文档有段伪代码,形象地说明了编译器对匿名名字空间的处理,匿名名字空间的定义会被替换为下面的代码:
inline namespace unique { /* empty body */ }
using namespace unique ;
namespace unique { namespace-body }
我们之所以能使用匿名名字空间内的名字,是因为编译器会自动生成using
的代码导出这些名字。
匿名名字空间用起来比较容易,有两个点需要特别说明下,作为本文的结束。
匿名名字空间定义放在哪合适
通常放在源代码里,放在头文件会有两个问题:
与惯例约定相违
头文件通常仅放对外导出的名字,由于匿名名字空间里名字是无法导出的,放在这里不合适。
可能会引发名字冲突
放在头文件就会被多个文件包含,存在一种情况:两个本来不同的匿名空间同时导入到了一个编译单元,而这两个匿名空间包含相同的名字,这样就会引起名字冲突。因为名字空间具有扩展性,编译器会认为导入到同一编译单元的匿名名字空间是同一个。
所以,匿名名字空间定义适合放在源代码。
匿名名字空间内的名字的连接性如何
这个问题涉及比较多的规则,我们简单地将问题落在上面例子上:check_header
具有外部连接还是内部连接?
网上好多文章支持具有外部连接,这个观点是错误的,C++17标准明确规则了这类名字具有内部连接,引用的标准原文如下:
A name having namespace scope (3.3.6) has internal linkage if it is the name of— a variable, function or function template that is explicitly declared static; or,— a non-volatile variable that is explicitly declared const or constexpr and neither explicitly declared extern nor previously declared to have external linkage; or— a data member of an anonymous union.
An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has internal linkage. All other namespaces have external linkage. A name having namespace scope that has not been given internal linkage above has the same linkage as the enclosing namespace if it is the name of— a variable; or— a function; or— a named class (Clause 9), or an unnamed class defined in a typedef declaration in which the class has the typedef name for linkage purposes (7.1.3); or— a named enumeration (7.2), or an unnamed enumeration defined in a typedef declaration in which the enumeration has the typedef name for linkage purposes (7.1.3); or— an enumerator belonging to an enumeration with linkage; or— a template.
标准的判断逻辑是这样的:匿名名字空间是内部连接的,由于check_header
不满足第一段内部连接的判断条件,且由于该名字指代函数,所以连接性跟匿名名字空间保持一致,即内部连接。