C++ 名称绑定与依赖名称
在 C++(尤其是模板编程)中,名称绑定 和 依赖名称 是编译器如何解析、查找和验证标识符(如变量名、函数名、类型名)的核心机制。
理解这两个概念,是掌握 C++ 模板编程、解释各种奇怪的模板编译错误(如“缺少 typename”或“未找到符号”)的关键。
一、依赖名称与非依赖名称
模板中使用两种名称:依赖名称和非依赖名称。前者是依赖模板参数的类型或值的名称,可以是类型参数、非类型形参或模板参数。不依赖于模板参数的名称称为非依赖名称。依赖名称和非依赖名称的查找方式不同:
- 依赖名称,在模板实例化时执行。
- 非依赖名称,则在模板定义时执行。
1.1 依赖名称
下面先看一下依赖名称的常见形式:
- 模板参数本身:
T - 受限的依赖名称:
T::member、typename T::type - 依赖类型的表达式:
std::vector<T>::iterator
下面看一个例子:
1 | |
可以看到:arg.handle(42) 这里调用了 handle 方法,并由于实参的类型是模板形参 T,所以 handle 成为了依赖名称。依赖名称在模板实例化时查找,因此句柄此时没有绑定。当 parser<handler<int>> p 实例化的时候,handle<int>::handle 会绑定到 arg.handle(42),使用的依赖名称的名称。
1.2 非依赖名称
下面看一个例子:
1 | |
handle 是一个非依赖名称,因为它不依赖于任何模板参数,所以此处执行名称查找和绑定。handle 必须是结构体 parser 已知的函数,因此在 (1) 处定义的函数是唯一匹配的。(2) 出定义的函数则是 (1) 的重载类型,能够更好地匹配 handle(42),但是它是在执行名称绑定之后出现的,因此将被忽略。
同理,将 (1) 和 (2) 的位置交换一下将得到结果:processing a int: 42。
二、两阶段的名称查找
名称查找对于依赖名称(依赖于模板参数的名称)和非依赖名称(不依赖于模板参数的名称,加上模板名称和当前模板实例化中定义的名称)不同。当编译器遍历模板定义时,需要判断名称是依赖的还是非依赖的,高阶名称查找依赖于这种分类,并且发生在模板定义点(对于非依赖名称)或模板实例化点(对于依赖名称)。因此,模板的实例化会分为两个阶段:
- 第一个阶段发生在定义时,检查模板语法并将名称分类为依赖或非依赖。
- 第二个阶段发生在实例化时,此时模板实参替换为模板参数。依赖名称的绑定这时发生。
这个分为两步的过程称为两阶段名称查找,来看一个例子:
1 | |
注意看,编译器将报错,因为它无法找到 init。发生这种情况的原因是 init 是一个不依赖的名称(不依赖于模板参数)。因此,必须在定义解析器模板时就知道。尽管 base_parser<T>::init 存在,但编译器不能假定它是我们想要的,因为主模板 base_parser 可以稍后进行特化,而 init 可以定义为其他东西(例如:类型、变量或另一个函数)。因此,名称查找不会发生在基类中,而只发生在其外围作用域中,并且 parser 中没有名为 init 的函数。
这个问题可以通过将init 设置为依赖名称来解决。这可以通过添加前缀 this-> 或 base_parser<T>:: 来实现。将 init 转换为依赖名称,其名称绑定将从模板定义点移动到模板实例化点。下面的代码段中,是通过 this 指针来解决问题的:
1 | |
再在上面代码的基础上加一个特化:
1 | |
三、依赖名称的类型
看下面的例子:
1 | |
value_type 无效,因为它是一个不依赖的名称,因此不会在基类中进行查找,只能在外围作用域中查找。base_parser<T>::value_type 也不能工作,因为编译器不能假设这是一个实际类型。base_parser 的特化可能紧随其后,所以 value_type 不一定是一种类型。
为了解决这个问题,需要告诉编译器这个名称指向的类型。否则,编译器默认会假定它不是类型。这可通过 typename 关键字在定义点完成,如下所示:
1 | |
实际上,这条规则有两个例外:
- 指定基类时
- 初始化类成员时
四、依赖模板的名称
某些情况下,依赖名称是模板,例如函数模板或类模板。但编译器的默认行为是将依赖项名称解释为非类型,这会导致使用小于比较操作符时出现错误:
1 | |
为了告诉编译器 base_parser 是一个成员模板,必须在 . 或 -> 或 :: 后面显式加上 template 关键字:
五、实例化
类模板定义的上下文中,可以避免使用 typename 和 template 关键字来消除依赖名称的歧义。在类模板定义的上下文中,编译器能够推导出一些依赖名称(例如嵌套类的名称)来引用当前实例化,所以一些错误可以在定义时(而不是实例化时)就可以找出来。
下面是可以引用当前实例化名称的完整列表:
| 内容 | 名称 |
|---|---|
| 类模板定义 | 嵌套类 类模板的成员 嵌套类的成员 注入模板的类名 注入的嵌套类的类名 |
| 主类模板定义 或 定义主类模板的成员 |
类模板的名称,后面跟着主模板 的模板实参列表,其中每个实参都 等效于其对应的形参 |
| 嵌套类或类模板的定义 | 用作当前实例化成员的嵌套类的名称 |
| 偏特化的定义 或 偏特化成员的定义 |
类模板的名称,后面跟着偏特化 的模板参数列表,其中每个参数 等效于其相应的形参 |
以下是将名称作为当前实例化的部分规则:
- 当前实例化或其非依赖基类中找到的非限定名称(不在作用域解析操作符
::)的右侧 - 限定名(出现在范围解析操作符的右侧
::),若其限定符(出现在范围解析操作符左侧的部分)命名当前实例化,并且在当前实例化或其非依赖基类中找到 - 类成员访问表达式中使用的名称,其中对象表达式是当前实例化,且名称在当前实例化或其非依赖基类中找到