29 / 80 · C++11 · 约 8 分钟
成员模板为何不能 virtual
成员函数模板不能声明为虚函数,它的某个特化也不会自动覆盖基类虚函数。但类模板里的普通非模板成员可以是虚函数。需要兼顾泛型入口与运行期扩展时,可把模板转换层和固定签名的虚接口分开。
区分两种不同的模板位置
template<class T> void send(T) 是成员函数模板,标准禁止给它加 virtual;即使实例化出 send(int),这个模板特化也不会因签名巧合而覆盖基类虚函数。需要覆盖时,必须声明一个真正的非模板成员,例如 void send(int) override。
另一方面,template<class T> struct Sink { virtual void send(T) = 0; }; 中 send 本身没有模板参数列表,是类模板里的普通成员,可以为虚函数。Sink<int> 实例化后,虚接口签名已经固定为 int;Sink<double> 是另一个不同的类类型。
限制是语言规则,不是虚表容量推导
模板按使用情境产生特化,虚函数则为已确定的类接口建立覆盖关系;两者的扩展时间和匹配规则不同。固定签名让独立编译的调用方与实现方能够约定同一个动态接口,常见 ABI 的虚表布局也围绕这种接口组织。
“模板可能无限多所以虚表装不下”可以作为动机直觉,但不是证明,更不能据此声称编译器理论上无法实现别的模型。准确回答应先引用成员模板不得 virtual 的规则,再说明类模板普通成员这个经常被混淆的合法情况。
泛型适配层加固定动态接口
示例提供非虚的 put<T> 模板,只负责把输入转成统一的 string,再调用固定签名的虚 write。调用者得到泛型便利,派生类只需要实现一个动态接口。转换可能失败时,应让错误在 write 前被报告,避免把不完整的数据交给实现。
这种设计是一种边界收敛:不是让运行期虚接口认识任意类型,而是把类型差异留在编译期适配层。如果数据不能无损转换成共同表示,可选择有限重载、variant 或有明确操作集合的类型擦除;不要为了看起来泛型而丢失业务语义。
容易答错的地方
- 派生类的同名成员模板可能隐藏基类函数,却不覆盖它;通过基类引用调用时仍按原虚接口分派。
- 一个成员不是模板,不代表其所属类也不是模板;判断 virtual 是否合法要看函数声明本身。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <sstream>
#include <stdexcept>
#include <string>
class Sink {
public:
template<class T>
void put(const T& value) {
std::ostringstream stream;
stream << value;
if (!stream) throw std::runtime_error("conversion failed");
write(stream.str());
}
virtual ~Sink() = default;
private:
virtual void write(const std::string& text) = 0;
};
class Buffer final : public Sink {
std::string data_;
void write(const std::string& text) override { data_ += text; }
public:
const std::string& data() const { return data_; }
};
int main() {
Buffer buffer;
Sink& sink = buffer;
sink.put(12);
sink.put(" apples");
assert(buffer.data() == "12 apples");
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread objects-virtual-template.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
基类声明 virtual void f(int),派生类只写 template<class T> void f(T),通过 Base& 调用 f(1) 会到哪里?如何转发到模板?
查看参考答案
该成员模板不会覆盖基类函数,因此虚调用仍到基类的最终覆盖者;若基类函数是纯虚,派生类仍抽象。应增加非模板的 void f(int value) override { f<int>(value); }。明确模板实参可选中模板特化,避免包装函数误调用自身而无限递归。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。