待决名

本节,我们开始讲待决名

待决名在模板当中无处不在,只要你写模板,一定遇到并且处理过它,即使你可能是第一次听到“待决名”这个名字。

待决名是你学习模板中重要的一个阶段,以此就可以划分,“新手写模板”和”正常写模板“,我们不但要知道,怎么写,不能怎么写,还要知道,为什么?

在模板(类模板和函数模板)定义中,某些构造的含义可以在不同的实例化间有所不同。特别是,类型和表达式可能会取决于类型模板形参的类型和非类型模板形参的值。

循序渐进,待决名的规则非常繁杂,我们用一个一个示例为你慢慢展示(只讲常见与重要的),从简单到困难。

待决名的 typename 消除歧义符

在模板(包括别名模版)的声明或定义中,不是当前实例化的成员且取决于某个模板形参的名字不会被认为是类型,除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)

先用一个示例引入问题。

template<typename T>
const T::type& f(const T&) {
    return 0;
}

struct X{
    using type = int;
};

int main(){
    X x;
    f(x);
}

以上代码会产生编译错误。

msvc 报错提示:

error C2061: 语法错误: 标识符“type”
error C2143: 语法错误: 缺少“;”(在“{”的前面)
error C2447: “{”: 缺少函数标题(是否是老式的形式表?)
error C2065: “x”: 未声明的标识符
error C3861: “f”: 找不到标识符

gcc


<source>:2:7: error: need 'typename' before 'T::type' because 'T' is a dependent scope
    2 | const T::type& f(const T&) {
      |       ^
      |       typename 
<source>: In function 'int main()':
<source>:12:5: error: 'f' was not declared in this scope
   12 |     f(x);
      |     ^

总的意思也很简单,编译器不觉得你这个 type 是一个类型

我知道此时,很多人会想到使用一个关键字:typename

我们只需要在 T::type& 前面加上 typename 就能够通过编译。

template<typename T>
const typename T::type& f(const T&) {
    return 0;
}

我们使用这个函数模板,来套一下,一句一句分析我们最开始说的那些概念。

在模板(包括别名模版)的声明或定义中

我们函数模板 f 自然是在模板中,符合。

不是当前实例化的成员且取决于某个模板形参的名字

我们的 T::type 的确不是当前实例化的成员,当前实例化的是函数模板 fT::type 的确是取决于我们的模板形参的名字,简单的说就是 T::type 是什么,取决于当前的函数模板。符合。

不会被认为是类型

是的,所以我们前面没有使用 typename 产生了编译错误。

除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)

是的,我们后面的示例使用了 typename 就没有问题了,T::type 被认为是类型了。


int p = 1;

template<typename T>
void foo(const std::vector<T>& v){
    // std::vector<T>::const_iterator 是待决名,
    typename std::vector<T>::const_iterator it = v.begin();

    // 下列内容因为没有 'typename' 而会被解析成
    // 类型待决的成员变量 'const_iterator' 和某变量 'p' 的乘法。
    // 因为在此处有一个可见的全局 'p',所以此模板定义能编译。
    std::vector<T>::const_iterator* p;

    typedef typename std::vector<T>::const_iterator iter_t;
    iter_t* p2; // iter_t 是待决名,但已知它是类型名
}

int main(){
    std::vector<int>v;
    foo(v); // 实例化失败
}

这个就不逐句解释了,就说一下最后一句注释:

iter_t 是待决名,但已知它是类型名

除非使用关键词 typename 或它已经被设立为类型名(例如用 typedef 声明或通过用作基类名)

值得一提的是,只有在添加 foo(v),即进行模板实例化后 gcc/clang 才会拒绝该程序; 如果你测试过 msvc 的话,会注意到,typedef typename std::vector<T>::const_iterator iter_t; 这一句,即使不加 typename 一样可以通过编译;

msvc 搞特殊,我们知道就行;不要只测 msvc,不然代码不可跨平台。

关键词 typename 只能以这种方式用于限定名(例如 T::x)之前,但这些名字不必待决

v 不是待决名,但是的确可以以 typename 修饰,虽然没啥用处。

typename std::vector<int>::value_type v;

到此,typename 待决名消除歧义符,我们已经讲清楚了所有的概念,其他各种使用无非都是从这些概念上的,不会有什么特殊。

待决名的 template 消除歧义符

与此相似,模板定义中不是当前实例化的成员的待决名同样不被认为是模板名,除非使用消歧义关键词 template,或它已被设立为模板名:

template<typename T>
struct S{
    template<typename U>
    void foo() {}
};
 
template<typename T>
void bar(){
    S<T> s;
    s.foo<T>();          // 错误:< 被解析为小于运算符
    s.template foo<T>(); // OK
}

使用 template 消除歧义更加少见一点,不过我们一句一句分析就行。

模板定义中不是当前实例化的成员的待决名同样不被认为是模板名

s.foo<T>() 的确是在模板定义,并且不是当前实例化的成员,它只是依赖了当前的模板实参 T ,所以不被认为是模板名。

除非使用消歧义关键词 template

s.template foo<T>() 的确。

注意:s.foo<T>() 在 msvc 可以被解析,通过编译,这是非标准的,知道即可。


关键词 template 只能以这种方式用于运算符 ::(作用域解析)、->(通过指针的成员访问)和 .(成员访问)之后,下列表达式都是合法示例:

  • T::template foo<X>();

  • s.template foo<X>();

  • this->template foo<X>();

  • typename T::template iterator<int>::value_type v;

与 typename 的情况一样,即使名字并非待决或它的使用并未在模板的作用域中出现,也允许使用 template 前缀。

struct X{
    template<typename T>
    void f()const {}
};
struct C{
    using Ctype = int;
};

X x;
x.template f<void>();
C::template Ctype I;

没有作用,但是合法

重复强调一下:template 的使用比 typename 少,并且 template 只能用于 ::->. 三个运算符 之后

绑定规则

对待决名和非待决名的名字查找和绑定有所不同

非待决名在模板定义点查找并绑定。即使在模板实例化点有更好的匹配,也保持此绑定

名字查找有非常复杂的规则,尤其还和待决名掺杂在一起,但是我们却不得不讲。

#include <iostream>

void g(double) { std::cout << "g(double)\n"; }

template<class T>
struct S{
    void f() const{
        g(1); // "g" 是非待决名,现在绑定
    }
};

void g(int) { std::cout << "g(int)\n"; }

int main(){
    g(1);  // 调用 g(int)

    S<int> s;
    s.f(); // 调用 g(double)
}

s.f() 中调用的是 g(1); 按照一般直觉会选择到 void g(int),但是实际却不是如此,它调用了 g(double)

非待决名在模板定义点查找并绑定。即使在模板实例化点有更好的匹配,也保持此绑定

查找规则

我们用 loser homework 第九题,引入我们的问题。

#include<iostream>

template<class T>
struct X {
    void f()const { std::cout << "X\n"; }
};

void f() { std::cout << "全局\n"; }

template<class T>
struct Y : X<T> {
    void t()const {
        this->f();
    }
    void t2()const {
        f();
    }
};

int main() {
    Y<void>y;
    y.t();
    y.t2();
}

以上代码的运行结果是:

X
全局

名字查找分为:有限定名字查找无限定名字查找

有限定名字查找指?

出现在作用域解析操作符 :: 右边的名字是限定名(参阅有限定的标识符)。 限定名可能代表的是:

  • 类的成员(包括静态和非静态函数、类型和模板等)

  • 命名空间的成员(包括其他的命名空间)

  • 枚举项

如果 :: 左边为空,那么查找过程只会考虑全局命名空间作用域中作出(或通过 using 声明引入到全局命名空间中)的声明。

this->f();

那么显然,这个表达式不是有限定名字查找,那么我们就去无限定名字查找中寻找答案。

我们找到模板定义

对于在模板的定义中所使用的非待决名,当检查该模板的定义时将进行无限定的名字查找。在这个位置与声明之间的绑定并不会受到在实例化点可见的声明的影响。而对于在模板定义中所使用的待决名它的查找会推迟到得知它的模板实参之时。此时,ADL 将同时在模板的定义语境和在模板的实例化语境中检查可见的具有外部连接的 (C++11 前)函数声明,而非 ADL 的查找只会检查在模板的定义语境中可见的具有外部连接的 (C++11 前)函数声明。(换句话说,在模板定义之后添加新的函数声明,除非通过 ADL 否则仍是不可见的。)如果在 ADL 查找所检查的命名空间中,在某个别的翻译单元中声明了一个具有外部连接的更好的匹配声明,或者如果当同样检查这些翻译单元时其查找会导致歧义,那么行为未定义。无论哪种情况,如果某个基类取决于某个模板形参,那么无限定名字查找不会检查它的作用域(在定义点和实例化点都不会)

很长,但是看我们加粗的就够:

  • 非待决名:检查该模板的定义时将进行无限定的名字查找

  • 待决名:它的查找会推迟到得知它的模板实参之时

我们这里简单描述一下:

this->f() 是一个待决名,这个 this 依赖于模板 X

所以,我们的问题可以解决了吗?

  1. this->f() 是待决名,所以它的查找会推迟到得知它模板实参之时(届时可以确定父类是否有 f 函数)。

  2. f() 是非待决名,检查该模板的定义时将进行无限定的名字查找(无法查找父类的定义),按照正常的查看顺序,先类内(查找不到),然后全局(找到)。

补充:如果是 msvc 的某些早期版本,或者 C++ 版本设置在 C++20 之前,会打印 X X。这是因为 msvc 不支持二阶段名字查找 Two-phase name lookup

总结

我们省略了很多的规则,这很正常,着重聊了几个重点,这足够各位的使用,如果还有需求,查阅文档

Last updated