面向对象与泛型编程在现代c++中是互补而非互斥的:运行时不确定类型用虚函数,编译期已知类型集合用模板;混合设计应分离接口(继承)与实现(模板),并用concept约束替代dynamic_cast,以实现统一接口与零成本抽象。

可以,而且在现代 C++ 工程中,面向对象(OOP)和泛型编程(GP)不是互斥选项,而是互补手段。关键不在于“能不能一起用”,而在于“怎么组合才不踩坑、不牺牲性能也不丢灵活性”。
什么时候该用虚函数,什么时候该用模板?
核心判断依据是:**多态行为是否在运行时决定**。
常见错误现象:std::vector<:unique_ptr>></:unique_ptr> 存了一堆子类对象,但所有操作都走虚函数调用 —— 性能敏感路径里反复查虚表,却没意识到其中部分类型在编译期就已知。
- 运行时不确定具体类型(如插件系统、用户配置加载的策略)→ 用继承 +
virtual函数 - 类型集合固定、可枚举(如支持
int/float/std::string的序列化器)→ 用模板特化或concept约束 - 既要统一接口,又要零成本抽象 → 把模板作为实现细节,继承作为对外契约(例如
ProtocolHandler基类 +TypedHandler<t></t>模板派生)
模板类继承虚基类的典型写法
这是混合设计最常被误写的部分。错误写法是让模板类直接 public 继承虚基类,却不处理虚析构或接口一致性。
正确做法是明确分离“接口”与“实现”:
class ProtocolHandler {
public:
virtual void handle(const std::string& data) = 0;
virtual ~ProtocolHandler() = default; // 必须有虚析构
};
<p>template <typename t>
class TypedHandler : public ProtocolHandler {
void handle(const std::string& data) override {
T parsed = parse<t>(data); // 编译期确定解析逻辑
process(parsed);
}
private:
void process(const T& obj); // 可内联,无虚调用开销
};</t></typename></p>
注意:TypedHandler<int></int> 和 TypedHandler<double></double> 是两个完全不同的类型,不能互相转换;但它们都能被 std::unique_ptr<protocolhandler></protocolhandler> 持有 —— 这就是混合的价值:对外统一,对内高效。
concept 约束模板参数比 dynamic_cast 更可靠
有人试图在运行时用 dynamic_cast 判断模板实例类型,这不仅低效,还暴露了设计漏洞。C++20 的 concept 才是正解。
比如你希望某个模板只接受“可序列化的类型”:
template <typename t>
concept Serializable = requires(T t) {
{ t.serialize() } -> std::convertible_to<:string>;
};
<p>template <serializable t>
class NetworkSender {
void send(const T& obj) {
auto data = obj.serialize(); // 编译期保证存在且返回 string
// ...
}
};</serializable></p></:string></typename>
比起在运行时抛 std::bad_cast,这种约束让错误提前到编译阶段,也避免了虚函数间接调用的不确定性。
混合设计真正的复杂点不在语法,而在职责划分的粒度:哪些逻辑必须延迟到运行时(比如从网络读取类型标识后再 dispatch),哪些其实可以在编译期收口(比如协议字段的字节序转换)。一旦边界模糊,就会出现模板爆炸、虚函数滥用、或者两者都用却谁也没用好。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











