Effective C++ 条款32:确定你的public继承塑模出 is-a 关系
Effective C++ 条款32:确定你的public继承塑模出is-a关系
核心思想:public继承必须严格遵循"is-a"关系(里氏替换原则),即派生类对象在任何场景下都必须是基类对象的逻辑子集。违反这一原则将导致设计错误和运行时异常。
⚠️ 1. is-a关系的内涵与要求
基本原则:
- 派生类对象可被当作基类对象使用
- 基类的所有行为都适用于派生类
- 派生类可扩展功能,但不能削弱基类契约
数学表达:
∀x (x∈Derived → x∈Base) // 派生类是基类的子集
代码验证:
void process(const Base& b); // 接受基类的函数Derived d;
process(d); // 必须能正确工作(里氏替换)
违反示例:
class Bird {
public:virtual void fly(); // 鸟会飞
};class Penguin : public Bird { // 企鹅是鸟,但不会飞!
public:void fly() override { throw CantFlyException(); // 违反is-a!}
};Penguin p;
processBird(p); // 可能抛出异常,破坏基类契约
🚨 2. 常见设计陷阱与解决方案
陷阱1:非普适行为继承
// 错误设计
class Rectangle {
public:virtual void setHeight(int h); // 可独立设置宽高
};class Square : public Rectangle {
public:void setHeight(int h) override {width_ = h; // 同时修改宽度height_ = h;}// 违反矩形行为契约!
};void stretch(Rectangle& r) {r.setHeight(r.getHeight() + 10); // 对正方形会意外修改宽度
}
解决方案:
// 正确设计:不继承
class Square {
public:void setSide(int s) { ... }
};// 关系建模
class Shape { /* 公共接口 */ };
class Rectangle : public Shape { ... };
class Square : public Shape { ... };
陷阱2:接口污染
class Airport { ... };// 错误设计
class Aircraft {
public:virtual void takeoff(Airport& dest) = 0;
};class HeliPad; // 直升机专用class Helicopter : public Aircraft {
public:void takeoff(HeliPad& pad); // 参数类型不兼容!
};
解决方案:
// 正确设计:分离接口
class Aircraft {
public:virtual void takeoff() = 0;
};class CommercialAircraft : public Aircraft {
public:void setDestination(Airport& ap);void takeoff() override;
};class Helicopter : public Aircraft {
public:void setLaunchPad(HeliPad& pad);void takeoff() override;
};
⚖️ 3. 最佳实践指南
场景 | 推荐方案 | 原因 |
---|---|---|
严格is-a关系 | ✅ 使用public继承 | 天然建模分类关系 |
共享接口但行为不同 | 🔶 接口继承+实现覆盖 | 保持多态行为一致 |
“has-a"或"is-implemented-in-terms-of” | ⛔ 避免继承,用组合 | 防止接口污染 |
运行时类型依赖 | ⚠️ 避免dynamic_cast | 通常是设计缺陷的信号 |
接口扩展 | ✅ 非虚拟接口模式(NVI) | 保持核心策略稳定 |
现代C++增强:
// 明确禁止覆盖(C++11)
class Base {
public:virtual void stableAPI() final; // 禁止派生类修改
};// 显式重写语法(C++11)
class Derived : public Base {
public:void stableAPI() override; // 错误!final函数不能override
};// 契约编程(C++20)
class Shape {
public:virtual void draw() [[expects: isValid()]] // 前置条件[[ensures: isDrawn()]] // 后置条件= 0;
};
💡 关键设计原则
-
里氏替换测试
- 派生类对象必须能替代基类对象
- 所有基类操作在派生类中保持语义一致
- 派生类不强化前置条件/不弱化后置条件
-
契约继承优先
- 继承行为契约而非具体实现
- 使用纯虚函数定义严格接口
- 模板方法模式控制流程
-
组合优于继承
// "has-a"关系使用组合 class Car { private:Engine engine_; // Car has-a Engine };// "is-implemented-in-terms-of"使用组合 class Set { private:std::list<int> impl_; // 用list实现set public:void insert(int v) {if(!contains(v)) impl_.push_back(v);} };
-
类型特征检查
// C++17编译时检查 template<typename T> void process(T obj) {static_assert(std::is_base_of_v<Base, T>, "T must inherit from Base");// ... }
危险模式重现:
class Database { public:virtual void open() = 0; };class MySQL : public Database { public:void open() override; // 需要连接参数? };void runReport(Database& db) {db.open(); // 对MySQL可能缺少必要参数 }
安全重构方案:
class Database { public:void open(const ConnectionParams& params) { // 非虚接口validate(params);doOpen(params); // 虚函数} private:virtual void doOpen(const ConnectionParams&) = 0; };class MySQL : public Database { private:void doOpen(const ConnectionParams& params) override {// 使用参数建立连接} };
多态安全场景:
class Animal { public:virtual void move() = 0; };class Bird : public Animal { public:void move() override { fly(); }virtual void fly() { /* 飞行实现 */ } };class Ostrich : public Bird { // 鸵鸟是鸟但不会飞 public:void fly() override {throw CannotFlyError(); // 设计错误!} };// 正确设计:分离会飞行为 class FlyingAnimal : public Animal { public:void move() override { fly(); }virtual void fly() = 0; };class Bird : public Animal { /* 不强制飞行 */ }; class Eagle : public Bird, public FlyingAnimal { ... }; class Ostrich : public Bird { /* 实现行走 */ };