继承类模板:函数未在模板定义上下文中声明,只能通过实例化上下文中参数相关的查找找到

案发现场:

还是先来看代码,给出具体的问题场景。

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成员/函数编译器的延后检查、按需实例化

相关推荐
-雷阵雨-1 天前
数据结构——优先级队列(堆)
java·开发语言·数据结构·intellij-idea
步行cgn1 天前
Java项目包结构设计与功能划分详解
java·开发语言·架构·mvc
ss2731 天前
手写MyBatis第92弹:SqlSource体系、SqlNode树与Trim标签实现原理全揭秘
java·开发语言
2501_915909061 天前
iOS 抓包工具有哪些?实战对比、场景分工与开发者排查流程
android·开发语言·ios·小程序·uni-app·php·iphone
charlie1145141911 天前
精读C++设计模式20 —— 结构型设计模式:桥接模式
开发语言·c++·学习·设计模式·桥接模式·c++23·概论
235161 天前
【LeetCode】46. 全排列
java·数据结构·后端·算法·leetcode·职场和发展·深度优先
未知陨落1 天前
LeetCode:90.最长有效括号
算法·leetcode
liangshanbo12151 天前
React 18 的自动批处理
前端·javascript·react.js
zzywxc7871 天前
AI工具应用全解析:智能编码、数据标注与模型训练的协同实践
人工智能·算法·信息可视化·自动化·ai编程
heyCHEEMS1 天前
最长连续序列 Java
java·开发语言·算法