Effective C++ 条款32:确定你的public继承塑模出 is-a 关系

发布于:2025-08-10 ⋅ 阅读:(23) ⋅ 点赞:(0)

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;
};

💡 关键设计原则

  1. 里氏替换测试

    • 派生类对象必须能替代基类对象
    • 所有基类操作在派生类中保持语义一致
    • 派生类不强化前置条件/不弱化后置条件
  2. 契约继承优先

    • 继承行为契约而非具体实现
    • 使用纯虚函数定义严格接口
    • 模板方法模式控制流程
  3. 组合优于继承

    // "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);
        }
    };
    
  4. 类型特征检查

    // 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 { /* 实现行走 */ };

网站公告

今日签到

点亮在社区的每一天
去签到