案发现场:
还是先来看代码,给出具体的问题场景。
cpp
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
push_back(x);
}
//void pop()
//{
//
// func();
//}
//const T& top()
//{
//
//}
//bool empty()
//{
//
//}
};
这是一段写在"继承"里的一段代码:我尝试定义一个类stack继承一个类模板------与平常继承一个具体的类不同。因为继承,派生类stack也是一个模板类;因为继承,派生类stack<T>继承了基类vector<T>的成员变量和成员函数,就包括了push_back(T)。于是,派生类stack<T>就可以顺理成章调用它继承来的push_back,这个push_back恰好就可以实现它需要的push功能。
于是,由于继承,由于功能的契合,一切都那么顺理成章。那么在我实例化出这个派生类后,是否能正常push呢?
注:为了达到效果,我先只留下push函数并进行测试。
测试代码如下:
cpp
void test3()
{
stack<int> s;
s.push(10);
}
我实例化了stack<int>类的对象s,接着我想入栈,也就是push一个10。
我们编译、运行一下:
图1 编译信息
一切源头都是 test.cpp(162)行:
图2 出错代码162
真相究竟是什么?
报错信息:"push_back": 找不到标识符"。据我前文分析:

现在这段话就一个词不对------"顺理成章"。
何出此言?
编译器是从上到下处理代码(预处理,编译,汇编,运行)。预处理:头文件展开/宏替换/条件编译/去掉注释;编译:语法分析、将代码转换成汇编代码。好,打住(详略得当(bushi)
看到这个处理顺序和方式,可以知道刚刚报错信息正是走到编译阶段语法分析失败的。
真相就是:编译器从头开始扫代码初步扫到模板定义阶段时,它有个规矩:凡是依赖模板参数的名字(dependent name),在模板定义阶段一律"延迟"到实例化时再查。而不依赖模板参数的T,编译器直接在当前可见域里找不到就报错。
如果这样写,就是"依赖模板参数的名字"
cpp
...ic:
void push(const T& x)
{
vector<T>::push_back(x);// 依赖模板参数T,加了作用域限定符
}
};
我们现在的版本,单看push_back(x)就不依赖模板参数T。编译器也不去猜你基类有没有这个函数了,直接认定这个函数(push_back)在当前类域里就有定义,就傻乎乎去找------没找到,自然就""push_back": 找不到标识符"。
为什么不去猜?因为它懒。其实不是,假如此时它还不报错,姑且它押宝在实例化基类后再实例化派生类,派生类继承了成员函数。它再去基类找,如果还是找不到,浪费效率还找不到,它就是哑巴吃黄连------所以,它选择依赖T的成员或者函数,就直接当作本作用域函数了。写程序的我们就应该提前给它来一镇定剂,标注好作用域限定符------让它先稍安勿躁,不要着急去检查push_back。等实例化(编译器扫到stack<int> s)后,派生类对象自然而然就继承了函数,顺理成章顺着继承树(vector<T>::这条线)就找到了。
图3 成功编译
图4 成功运行
终究是错付了---按需实例化
如果按上面这种检查机制,那么我可以这样戏耍编译器------给一个本身不是基类的成员函数,也更没有在派生类里定义的函数,加上基类作用域限定符。这样编译器就不会在刚扫描这行代码报错。
但我们知道在实例化后编译器后面就会追查到这个函数并不存在。
这么说也就这么做:
cpp
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
vector<T>::push_back(x);
}
void pop()
{
// func()我胡乱取的,为了躲过编译器的编译,我加了vector<T>::,骗了它:等它实例化出stack<int> s就会找到func()
vector<T>::func();
}
};
cpp
void test3()
{
stack<int> s;
s.push(10);
s.pop();// 调用一下,肯定要露馅。
}
图5 编译报错
罢了罢了,意料之中。我们现在尝试,不调用pop()呢?
cpp
void test3()
{
stack<int> s;
s.push(10);
}
图6:不调用pop(),成功编译
图7 运行成功
编译骗过去了,运行成功了。------ 反推回去,只能说明:编译器并没有回去沿着vector<T>去检查func()是否如实存在。在stack<int> s;实例化了基类vector<T>,再实例化了派生类stack<T>;在s.push();实例化出了 stack<int>::push
-
在该函数体里,遇到 vector<T>::push_back(x);
→ 此时第一次需要 vector<int>::push_back 的定义,于是编译器当场实例化 vector<int>::push_back。
而我们说的检查vector<T>::push_back是否存在也在这个实例化过程得以完成------拿到了函数的定义,实例化成功。而原本等待后期检查vector<T>::func()函数,编译器也不会返回检查了。------正是因为没有调用s.pop(),不会触发stack<int>::pop()实例化,接着func()的实例化。
编译器:你骗得我好苦,但你没调用,也无伤大雅------如果你一旦调用,我让你编不过去。
不开玩笑了,这就是模板"按需实例化"------只有被真正用到的模板成员(包括成员函数、成员类、基类子对象)才会被实例化; 没被调用的成员,编译器连看都不看。
本章浓缩☕:依赖模板参数T成员/函数编译器的延后检查、按需实例化