第9章 模板中的名称
------------------------------------------------------------------------------------------------------------
C++(与C一样)是一种上下文相关语言:对于C++的一个构造,我们不能脱离它的上下文来理解它。模板也是一种构造,它必须处理多种上下文相关信息:
(1)模板出现的上下文;
(2)模板实例化的上下文;
(3)用来实例化模板的模板实参的上下文。
 9.1 名称的分类
 主要的命名概念:

(1)如果一个名称使用域解析运算符(即::)或者成员访问运算符(即 . 或 ->)来显式表明它所属的作用域,我们就称该名称为受限名称。

(2)如果一个名称(以某种方式)依赖于模板参数,我们就称它为依赖型名称。

名称的分类详见表9.1

9.2 名称查找

这里是讨论一些主要概念.

1. 受限名称的名称查找是在一个受限作用域内部进行的,该受限作用域由一个限定的构造所决定。如果该作用域是一个类,那么查找范围可以到达它的基类;但不会考虑它的外围作用域。如下例子:

int x;

class B
{
public:
int i;
}; class D : public B
{
}; void f(D* pd)
{
pd->i = ; // 找到B::i
D::x = ; // 错误:并不能找到外围作用域中的::x
}

2. 非受限名称的查找则相反,它可以(由内到外)在所有外围类中逐层地进行查找(但在某个类内部定义的成员函数定义中,它会先查找该类和基类的作用域,然后才查找外围类的作用域)。这种查找方式也被称为普通查找。如下:

3. 对于非受限名称的查找,最近增加了一项新的查找机制——除了前面的普通查找——就是说非受限名称有时可以使用依赖于参数的查找(argument-dependent lookup,ADL)。在阐述ADL的细节之前,让我们先通过max()模板来说明这种机制的动机:

template <typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}

假设我们现在要让“在另一个名字空间中定义的类型”使用这个模板函数:

namespace BigMath{
class BigNumber
{
...
};
bool operator < (BigNumber const&, BigNumber const&);
...
}
using BigMath::BigNumber; void g(BigNumber const& a, BigNumber const& b)
{
...
BigNumber x = max(a, b);
...
}

问题是max()模板并不知道BigMath名字空间,因此普通查找也找不到“应用于BigNumber类型值的operator<”。ADL正是解决这种限制的特殊规则。

9.2.1 Argument-Dependent Lookup(ADL)

ADL只能应用于非受限名称。在函数调用中,这些名称看起来像是非成员函数。对于成员函数名称或者类型名称,如果普通查找能找到该名称,那么将不会应用ADL。如果把被调用函数的名称(如max)用圆括号括起来,也不会使用ADL。

否则,如果名称后面的括号里面有(一个或多个)实参表达式,那么ADL将会查找这些实参的associated class(关联类)和associated namespace(关联名字空间)。

对于给定类型,对于由associated class(关联类)和associated namespace(关联名字空间)所组成的集合的准确定义,可以通过下列规则来确定:
(1)对于基本类型,该集合为空集。
(2)对于指针和数组类型,该集合是所引用类型(譬如对于指针而言,它所引用的类型是“指针所指对象”的类型)的associated class和associated namespace。
(3)对于枚举,associated namespace指的是枚举声明所在的namespace。对于类成员,associated class指的是它所在的类。
(4)对于class类型(包含联合类型),associated class集合包括:该class类型本身、它的外围类型、直接基类和间接基类。associated namespace集合是每个associated class所在的namespace。如果这个类是一个类模板实例化体,那么还包含:模板类型实参本身的类型、声明模板的模板实参所在的class和namespace。
(5)对于函数类型,该集合包括所有参数类型和返回类型的associated class和associated namespace。
(6)对于类X的成员指针类型,除了包括成员相关的associated namespace和associated class,该集合还包括与X相关的associated namespace和associated class。

至此,ADL会在所有的associated class和associated namespace中依次地查找,就好像依次地直接使用这些名字空间进行限定一样。唯一的例外情况是:它会忽略using-directives(using指示符)。

9.2.2 友元名称插入 考虑下面代码:

template <typename T>
class C
{
...
friend void f();
friend void f(C<T> const&);
....
}; void g(C<int>* p)
{
f(); // f()在此是可见的吗?不可见,不能利用ADL,因此是一个无效调用
f(*p); // f(C<int> const&)在此是可见的吗?可见,因为友元函数所在的类属于ADL的关联类集合
}

这里的问题是:如果友元声明在外围类中是可见的,那么实例化一个类模板可能会使一些普通函数(例如f())的声明也成为可见的。一些程序员会认为这样很出乎意料。因此C++标准规定:通常而言,友元声明在外围(类)作用域中是不可见的。

但同时,C++标准还规定:如果友元函数所在的类属于ADL的关联类集合,那么我们在这个外围类是可以找到该友元声明的。

9.2.3 插入式类名称

如果在类本身的作用域中插入该类的名称,我们就称该名称为插入式类名称。它可以被看作位于该类作用域中的一个非受限名称,而且是可访问的名称。

类模板也可以具有插入式类名称。然而,它们和普通插入式类名称有些区别:它们的后面可以紧跟模板实参(在这种情况下,它们也被称为插入式类模板名称)。但是,如果后面没有紧跟模板实参,那么它们代表的就是用参数来代表实参的类(例如,对于局部特化,还可以用特化实参代表对应的模板实参)。这同时说明了下面的情况:

template <template<typename> class TT> class X{ };

template <typename T> class C
{
C* a; // 正确:等价于C<T>* a
C<void> b; // 正确
X<C> c; // 错误:后面没有模板实参列表的非受限名称C不被看作模板
X<::C> d; // 错误:<: 是 [ 的另一种标记(表示)
X< ::C> e; // 正确:在 < 和 ::之间的空格是必需的
};

从上面代码我们可以知道如何使用非受限名称来引入插入式名称(即C),如果这些非受限名称的后面没有紧跟模板实参列表,那么是不会被看成模板名称的。

9.3 解析模板

大多数程序设计语言的编译都包含两个最基本的步骤:符号标记——和解析。扫描过程把源代码当作字符串序列读入,然后根据该序列生成一系列标记。接下来,解析器会递归地减少标记,或者把前面已经找到的模式结合成更高层次的构造,从而在标记序列中不断对应已知模式。

9.3.1 非模板中的上下文相关性
C++编译器会使用一张符号表把扫描器和解析器结合起来,解决上下文相关性的问题。当解析某个声明的时候,该声明就会添加到表中。当扫描器找到一个标识符时,它会在符合表中进行查找,如果发现该标识符是一个类型,就会注释这个所获得的标记(标识符)。
9.3.2 依赖型类型名称
有关模板名称的问题主要是:这些名称不能有效地确定。尤其是模板中不能引用其他模板的名称,因为其他模板的内容可能会由于显式特化而使原来的名称失效。

C++的语言定义通过下面规定来解决这个问题:通常而言,依赖型受限名称并不会代表一个类型,除非在该名称的前面有关键字typename前缀。总之,当类型名称具有以下性质时,就应该在该名称前面添加typename前缀:
(1)名称出现在一个模板中;
(2)名称是受限的;
(3)名称不是用于指定基类继承的列表中,也不是位于引入构造函数的成员初始化列表中;
(4)名称依赖于模板参数。

而且,只有当前面3个条件满足的情况下,才能使用typename前缀。如下例子:

template <typename1 T>
struct S : typename2 X<T>::Base
{
S() : typename3 X<T>::Base(typename4 X<T>::Base() ) {}
typename5 X<T> f()
{
typename6 X<T>::C *p; // 指针p的声明
X<T>::D* q; // 乘积
}
typename7 X<int>::C *s;
}; struct U
{
typename8 X<int>::C *pc;
};

注:
typename1引入模板参数,因此不适用前面的规则;
typename2和typename3属于规则(3)所禁止的用法;
typename4必不可少;
typename5属于规则(2)所禁止的用法;
typename6如果是期望声明一个指针,那么这个typename就是必需的;
typename7是可选的,因为它符合前面的3条规则,但不符合第4条规则;
typename8是禁止的,因为它并不是在模板中使用。

9.3.3 依赖型模板名称
如果一个模板名称是依赖型名称,我们将会遇到与上一小节类似的问题。通常而言,C++编译器会把模板名称后面的<看作模板参数列表的开始;但如果该<不是位于模板名称后面,那么编译器将会把它当作小于号处理。和类型名称一样,要让编译器知道所引用的依赖型名称是一个模板,需要在该名称前面插入template关键字,否则的话编译器将假定它不是一个模板名称:

template <typename T>
class Shell
{
public:
template<int N>
class In
{
public:
template<int M>
class Deep
{
public:
virtual void f();
};
};
}; template<typename T, int N>
class Weird
{
public:
void case1(typename Shell<T>::template In<N>::template Deep<N>* p){
p->template Deep<N>::f(); // 禁止虚函数调用(具体原因后面针对限定符部分讲解)
}
void case2(typename Shell<T>::template In<N>::template Deep<N>* p){
p.template Deep<N>::f(); // 禁止虚函数调用
}
};

这个多少有些复杂的例子给出了何时需要在运算符(::, ->和 . ,用于限定一个名称)的后面使用关键字template。更明确的说法是:如果限定符号前面的名称(或者表达式)的类型要依赖于某个模板参数,并且紧接在限定符后面的是一个template-id(就是指一个后面带有尖括号内部实参列表的模板名称),那么就应该使用关键字template。例如,在下面的表达式中:

p.template Deep<N>::f()

p的类型要依赖于模板参数T。然而,C++编译器并不会查找Deep来判断它是否是一个模板:因此我们必须显式指定Deep是一个模板名称,这可以通过插入template前缀来实现。如果没有这个前缀的话,p.Deep<N>::f()将会被解析为((p.Deep) < N ) > f(),这显然并不是我们所期望的。我们还应该看到:在一个受限名称内部,可能需要多次使用关键字template,因为限定符本身可能还会受限于外部的依赖型限定符(我们可以从前面例子中case1和case2的参数中看到这一点)。

9.3.4 using-declaration 中的依赖型名称

using-declaration 会从两个位置(即类和名字空间)引入名称。如果引入的是名字空间,将不会涉及到上下文问题,因为并不存在名字空间模板。实际上,从类中引入名称的using-declaration 的能力是有限的:只能把基类中的名称引入到派生类中。如下:

class BX
{
public:
void f(int);
void f(char const*);
void g();
}; class DX : private BX
{
public:
using BX::f;
};

私有继承中,通过using-declaration 访问基类的成员,但是这违背了C++早期的访问级别声明机制,所以可能以后不会包含这个机制。

现在,当using-declaration是从依赖型类(模板)中引入名称的时候,我们虽然知道这个引入的名称,但并不知道该名称究竟是一个类型名称、模板名称、还是一个其他的名称:

template <typename T>
class BXT
{
public:
typedef T Mystery;
template <typename U>
struct Magic;
}; template <typename T>
class DXTT : private BXT<T>
{
public:
using typename BXT<T>::Mystery;
Mystery* p; // 如果上面不使用typename,将会是一个语法错误
};

而且,如果我们期望使用using-declaration 所引入的依赖型名称是一个类型,我们必须插入关键字typename来显式指定。另一方面,比较奇怪的是,C++标准并没有提供一种类似的机制,来指定依赖型名称是一个模板。如下:

template <typename T>
class DXTM : private BXT<T>
{
public:
using BXT<T>::template Magic; // 错误:非标准的
Magic<T>* plink; //语法错误:Magic并不是一个已知模板
};

这应该是标准规范的一个疏忽。
 9.3.5 ADL和显式模板实参
考虑下面例子:

namespace N{
class X
{
...
};
template<int I> void select(X*);
} void g(N::X* xp)
{
select<>(xp); // 错误:没有ADL
}

在这个例子中,调用select<3>(xp)的时候,我们可能会期望通过ADL来找到模板select();然而,实际情况并不是这样的。因为编译器在不知道<3>是一个模板实参列表之前,是无法断定xp是一个函数调用实参的;反过来,如果要判断<3>是一个模板实参列表,我们需要先知道select()是一个模板。这种是先有鸡还是先有蛋的问题没法解决,因此编译器只能把上面表达式解析成(select<3)>(xp),但这并不是我们所期望的,也是毫无意义的。

9.4 派生和类模板

类模板可以继承也可以被继承。

9.4.1 非依赖型基类

在一个类模板中,一个非依赖型基类是指:无需知道模板实参就可以完全确定类型的基类。就是说,基类名称使用非依赖型名称来表示的。如下:

template<typename X>
class Base
{
public:
int basefield;
typedef int T;
};
class D1 : public Base<Base<void> > // 实际上不是模板
{
public:
void f() { basefield = ; }
}; template<typename T>
class D2 : public Base<double> // 非依赖型基类
{
public:
void f() {basefield = ; } // 正常访问继承成员
T strange; // T是Base<double>::T, 而不是模板参数
};

模板中的非依赖型基类的性质和普通非模板类中的基类的性质很相似,但存在一个很细微的区别:对于模板中的非依赖型基类而言,如果在它的派生类中查找一个非受限名称,那就会先查找这个非依赖型基类,然后才查找模板参数列表。这就意味着:在前面的例子中,类模板D2的成员strange的类型一直都会是Base<double>::T中对应的T类型(个人理解:因为首先查找了非依赖型基类Base<double>,所以得到的T的类型就一直是Base<double>::T的类型。如果是普通非模板类的话,那么会首先在派生类自己中查找,也即,可以找到如D2<int>::T的类型。还是不太理解,待求证??)。例如,下面的函数是无效的C++代码:

void g(D2<int*>& d2, int* p)
{
d2.strange = p; // 错误,类型不匹配
}

这一违背直观查找的特性是我们要格外注意的。

9.4.2 依赖型基类

在前面的例子中,基类是完全确定的,它并不依赖于模板参数。这就意味着:一看到模板的定义,C++编译器就可以在这些基类中查找非依赖型名称。而另一种候选方法(C++标准并不允许这种方法)会延迟这类名称的查找,只有等到进行模板实例化时,才真正查找这类名称。这种候选方法的缺点是:它同时也将诸如漏写某个符号导致的错误信息,延迟到实例化的时候产生。因此,C++标准规定:对于模板中的非依赖型名称,将会在看到的第一时间进行查找。有了这个概念之后,让我们考虑下面的例子:

template <typename T>
class DD : public Base<T>
{
public:
void f() { basefield = ; } // (1)problem……
}; template<> // 显式特化
class Base<bool>
{
public:
enum { basefield = }; // (2)tricky
}; void g(DD<bool>& d)
{
d.f(); // (3)oops ?
}

在(1)处我们发现代码中引用了非依赖型名称basefield,必须马上对它进行查找。假设我们在模板Base中查找到它,并根据Base类的声明把basefield绑定为int变量。然而,我们随后使用显式特化改写了Base的泛型定义,在特化中改变了成员basefiel的含义,而(1)处basefield的含义在这之前已确定下来了(即绑定为一个int变量);这也是错误的根源。因此,当我们在(3)处实例化DD::f的定义时,我们会发现过早地在(1)处绑定了非类型名称;然而根据(2)处对DD<bool>的特殊指定,basefield应该是一个不可修改的常量,因此编译器在(3)处将会给出一个错误的信息。
为了(巧妙地)解决这个问题,标准C++声明:非依赖型名称不会在依赖型基类中进行查找(但仍然是在看到的时候马上进行查找)。因此,标准的C++编译器将会在(1)处给出一个诊断信息。为了纠正这里的代码,我们可以让basefield也成为依赖型名称,因为依赖型名称只有在实例化时才会进行查找;而且在实例化时,基类的特化是已知的。例如,在(3)处,编译器知道DD<bool>的基类是Base<bool>,而且Base<bool>是程序员进行显式特化的。在这个例子中,我们可以借助如下的修改方案是basefield成为一个依赖型名称:

// 修改方案1
template <typename T>
class DD1 : public Base<T>
{
public:
void f() { this->basefield = ; } // 查找被延迟了
}; // 修改方案2:利用受限名称来引入依赖性
template <typename T>
class DD2 : public Base<T>
{
public:
void f() { Base<T>::basefield = ; }
};

如果是使用这个解决方法,我们需要格外小心,因为如果(原来的)非受限的非依赖型名称是被用于虚函数调用的话,那么这种引入依赖性的限定将会禁止虚函数调用,从而也会改变程序的含义(详见下一篇)。因此,当遇到第2种解决方案不适用的情况,我们可以使用方案1。

最后提供第3个修改方案如下:

// 修改方案3:重复的限定让代码不雅观,可以在派生类中只引入依赖型基类
template <typename T>
class DD3 : public Base<T>
{
public:
using Base<T>::basefield; // (1)依赖型名称现在位于作用域
void f() { basefield = ; } // 正确
};

更多有关模板名称查找和ADL机制的内容参见本系列下一篇博文xxxx。

C++ template —— 模板中的名称(三)的更多相关文章

  1. wepy框架 怎么在template模板中使用函数

    呵呵.介绍说是类似vue,用起来真累人,就想在模板中使用个函数都要查N久的文档才知道. 具体要怎么操作呢? 要先创建个wxs脚本文件,在里面定义函数或其它的,然后在页面或组件中引入这文件,就可以在模板 ...

  2. Win10系列:JavaScript 项目模板中的文件和项模板文件

    通过上面内容的学习,相信读者已经对各种项目模板和项模板有了大致的了解,本节将进一步介绍项目模板中默认包含的项目文件以及项模板文件,首先讲解这些文件中的初始内容以及作用,然后介绍在一个页面中如何添加控件 ...

  3. 微信小程序template模板与component组件的区别和使用

    前言: 除了component,微信小程序中还有另一种组件化你的方式template模板,这两者之间的区别是,template主要是展示,方法则需要在调用的页面中定义.而component组件则有自己 ...

  4. freemarker 模板中定义变量

    在模板中能够定义三种类型的变量: 简单变量:它能从模板中的不论什么位置来訪问,或者从使用 include 指令引入的模板訪问. 能够使用 assign 或 macro 指令来创建或替换这些变量. 局部 ...

  5. <Django> MVT三大块之Template(模板)

    1.模板简介 创建项目,基本配置 第一步:配置数据库 第二步:创建APP,配置APP 第三步:配置模板路径 第四步:配置分发urls.py(APP里面的) 根目录下,增加命名空间namespace,作 ...

  6. ArcGIS API for Silverlight代码中使用Template模板

    原文:ArcGIS API for Silverlight代码中使用Template模板 在项目开发中,会遇到点选中聚焦闪烁效果,但是因为在使用Symbol的时候,会设置一定的OffSetX和OffS ...

  7. 在Intellij IDEA中修改模板中user变量名称

    在Intellij IDEA中的注释模板中的${user}名称是根据当前操作系统的登录名来取的,有时候登录名称和我们实际的user名称并不相同. 修改方法如下: 方法一:可以在settings的fil ...

  8. C++对象模型——Template中的名称决议方式 (第七章)

    Template中的名称决议方式 (Name Resolution within a Template) 必须可以区分下面两种意义,一种是C++ Standard所谓的"sope of th ...

  9. go语言template包中模板语法总结

    package main; import ( "html/template" "os" "fmt" ) type Person struct ...

随机推荐

  1. 关于Unity中自带摇杆与车轮碰撞器的使用

    准备 在创建好项目目录的基础上 导入一个第三方的资源包,在Project面板里面 右键---->Import Package---->Custom Package---->easy_ ...

  2. 百度搜索_如何打开Intellij IDEA的代码提示功能?

    Intellij IDEA是一款优秀的编程软件,相比较Eclipse之下它的用户群较小,但并不代表它的功能就比Eclipse差,如果用顺手了还是特别好用的.代码提示功能对于程序员来说非常重要,那么我们 ...

  3. linux之sshfs

    1.挂载 sshfs -p shiyu@ml.cs.tsinghua.edu.cn:/mfs/shiyu/ ~/mfs 2.卸载 fusermount -u ~/mfs

  4. linux下查看目录下某种文件类型累计的代码行数

    find 路径 -name '*.py' | xargs wc -l

  5. C语言中的数组与字符串

    1. 数组与指针: 对于数组,需要注意两点:1, C语言中只有一维数组, 而且数组的大小必须在编译期就作为一个常数确定下来: 2. 对于一个数组,我们只能做两件事:确定数组的大小 和 获得指向该数组下 ...

  6. 层层递进——宽度优先搜索(BFS)

    问题引入 我们接着上次“解救小哈”的问题继续探索,不过这次是用宽度优先搜索(BFS). 注:问题来源可以点击这里 http://www.cnblogs.com/OctoptusLian/p/74296 ...

  7. 多线程系列四:AQS-AbstractQueuedSynchronizer

    什么是AbstractQueuedSynchronizer?为什么我们要分析它?  AQS:抽象队列同步器,原理是:当多个线程去获取锁的时候,如果获取锁失败了,当前线程就会被打包成一个node节点放入 ...

  8. Tomcat 部署一工程时Deploy Location 为什么 是 INVALID

    1.eclipse项目: 在项目的.settings目录下,找到org.eclipse.wst.common.component文件: <?xml version="1.0" ...

  9. 使用selenium遇到java.lang.NoSuchMethodError: org.apache.xpath.XPathContext,排查

    初试selenium webdriver,运行小程序,抛如下错误:   java.lang.NoSuchMethodError: org.apache.xpath.XPathContext.<i ...

  10. 于erlang依赖的linux调优

    [皇室]杭州-sunface(61087682) 上午 9:42:02 http://docs.basho.com/riak/latest/ops/tuning/linux/ 这篇文章对于erlang ...