CTTCG 13 Names in Templates
名字查找:普通查找和 ADL
普通查找和 ADL 是同时进行的,不存在优先级差异。这些被找到的函数被放在一起参与重载决议。如果没有因为一般性的重载决议规则决出优劣,则会引发 ambiguous 指代错误。
Ordinary lookup: 内部名称遮盖外部名称的规则
ADL: 考虑参数类型所在名字空间的函数和操作符重载
相关类型有很多规则,比如指针会引入其指向类型的名字空间、类会引入外围类(如果是一个内部类)和基类的名字空间、函数会引入其参数和返回值的名字空间等(P219)。特例是 hidden friends,可以直接定义在类中。
ADL 失效的情况:① 调用前给函数名加上括号,这样就必须先找到函数。② 函数模板在 C++20 之前除非在外部声明函数模板(参数列表可以不同),否则也无法被找到。因为在还没有看参数的情况下,函数模板没有被引入;还没有看参数列表的情况下不知道这个是函数模板;不知道这个是函数模板的情况下会把参数列表的头尾理解成大于和小于操作符。这是一种鸡生蛋蛋生鸡的问题。③ ADL 会忽略 using 声明,只考虑真正存在于名字空间的函数。
ADL 只会通过参数(也就是显式调用)引入额外的函数和操作符,其他情况下这些名称是不会被自动引入的,比如取一个和参数相关的函数指针。
名称注入
术语
在名字查找中,enclosing scope 指外围作用域。
非限定名(non-qualified)指的是前面没有 ::
的名称。限定名相反。
Friend Name Injection(友元函数)
hidden friends 就是友元名注入中完全把函数定义写在类中的特例。但事实上 friend name injection 只对 ADL 有效,并不会真的注入到 enclosing scope 中。
Class Name Injection
定义一个类时,类名会作为非限定名被向内注入到类的定义体中(限定名 T::T
则用来表示构造函数名)。
BTW,sizeof 优先找到变量名而不是类名:
类模板的情况会复杂一些。向内注入的非限定名既可以指模板,又可以指模板类。如果想要显式表示模板,可以使用限定名。
Current Instantiation 和 Unknown Specialization
💡 current instantiation 对模板参数有依赖(比如隐式的 T
)。unknown specialization 对模板参数不一定有依赖,但是因为不知道是否有特化存在所以本次无法生成(比如 C<int>
)。
类模板中省略参数,或者使用与本类模板完全一致的参数,被称为 current instantiation。而使用其他参数被称为 unknown specialization。因为类模板可以特化,在链接完成之前,不知道其他参数的类有什么内容,无法做出假定。比如:
类模板 C 可以被特化,因而其中的 I 和 J 可以被给出完全不同的定义,C<T>::J
中的 I* i;
语句也不知道指的是什么样的 I
。
依赖名
依赖于模板参数的名就叫依赖名。
💡 非限定名可以很明显的看出来是否依赖模板参数,要么被解释成依赖名在第二阶段处理,要么作为非依赖名在第一阶段处理。限定名如果可能存在依赖,则作为依赖名在第二阶段处理;否则作为非依赖名在第一阶段处理。P240
如果类模板的基类中有依赖于模板参数的,则显式 this 指针(限定名)也构成一种依赖。
使用 typename 和 template 关键字标注依赖名
由于不知道模板参数,类模板编译时无法对类模板的成员做出判断,现在的解决方法是把类型依赖名理解成值,除非用 typename 前缀显式表示类型,或者用 template 前缀显式表示模板。下图中灰色表示是否使用 typename 都是可以的,黄色表示必须使用,删除线表示不能使用:
一个例外是 using 声明。依赖类模板参数的 using 声明中可以使用 typename 关键字,但是不能使用 template 关键字。一种迂回的方式是创建一个 alias template 完成相同的事情。
💡 namespace a = b;
对名字空间重命名,using c = d;
对类型重命名。而对模板重命名需要使用 alias template,这比前两个更复杂一点。
依赖的类型、两阶段查找
type dependent 指的是类型和模板参数有关;value dependent 指的是值和模板参数有关;instantiation dependent 指的是实例化与模板参数有关。
类型依赖是最严格的,遇到类型依赖时,该表达式只能在 two-phase lookup 的第二阶段进行翻译。对于其他依赖在第一阶段能被检查出来的错误,编译器可以报也可以等到实例化再报,没有强制要求。非依赖非限定名必须在第一阶段查找到。在类模板的成员函数中写上 this 指针就得到了依赖名,从而能够在依赖的基类中启用查找(必须先有一个依赖基类否则不会进去找)。
例 1
上面出现了实例化依赖,因为 sizeof
操作符要求参数为完整类型。如果实例化这里时 T 不是一个完整的类型,就无法通过编译。(这里不存在类型依赖,因为 sizeof
的返回值是 size_t
,也不存在值依赖,因为内层的 sizeof
返回类型是 size_t
,外围的 sizeof
能够得出确定的值。)
例 2
没有出现任何依赖关系,因为上面代码的数组的长度会是负数,这段代码在函数未实例化时也不能通过编译。
继承
模板类从非依赖的基类继承的类型名会覆盖模板参数名。所以非模板参数也使用 T 这样的类型声明比较危险。