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

发布于:2025-09-15 ⋅ 阅读:(13) ⋅ 点赞:(0)

案发现场:

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

	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函数并进行测试。

测试代码如下:

	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,编译器直接在当前可见域里找不到就报错。

如果这样写,就是“依赖模板参数的名字

	...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 成功运行

终究是错付了—按需实例化

如果按上面这种检查机制,那么我可以这样戏耍编译器——给一个本身不是基类的成员函数,也更没有在派生类里定义的函数,加上基类作用域限定符。这样编译器就不会在刚扫描这行代码报错。

但我们知道在实例化后编译器后面就会追查到这个函数并不存在。

这么说也就这么做:

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();
		}
};
	void test3()
	{
		stack<int> s;
		s.push(10);
        s.pop();// 调用一下,肯定要露馅。
	}
图5 编译报错

罢了罢了,意料之中。我们现在尝试,不调用pop()呢?

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