当前位置: 首页 > news >正文

Effective C++ 条款36: 绝不重新定义继承而来的非虚函数

Effective C++ 条款36:绝不重新定义继承而来的非虚函数


核心思想非虚函数在继承体系中代表不变性(invariant),重新定义继承的非虚函数会破坏"is-a"关系,导致对象行为不一致、多态失效和代码脆弱性。这是C++中绝对禁止的设计错误。

⚠️ 1. 非虚函数的本质与风险

设计意义

  • 非虚函数表示不变行为:所有派生类必须严格遵循的实现
  • 体现类不变式(class invariant):对象生命周期内必须保持的特性
  • 保证行为一致性:无论通过基类还是派生类接口调用,行为必须相同

违反后果

class Base {
public:void log() const { // 非虚函数std::cout << "Base log entry\n"; }
};class Derived : public Base {
public:void log() const { // 错误:重新定义非虚函数!std::cout << "Derived log entry\n";}
};// 使用场景
Derived d;
d.log();           // 输出 "Derived log entry"Base* pb = &d;
pb->log();         // 输出 "Base log entry"!行为不一致Base& rb = d;
rb.log();          // 输出 "Base log entry"!静态绑定

问题分析

  1. 破坏多态:通过基类指针/引用调用时,总是调用基类版本
  2. 违反LSP:派生类对象无法替换基类对象(行为改变)
  3. 代码脆弱
    • 函数隐藏:Derived::log隐藏了Base::log而非覆盖
    • 调用困惑:相同对象不同接口表现不同

🚨 2. 错误模式与正确实践

常见错误场景

class Account {
public:void withdraw(double amount) { // 非虚函数if (amount > balance_) throw InsufficientFunds();balance_ -= amount;}
};class CheckingAccount : public Account {
public:void withdraw(double amount) { // 错误重定义// 添加手续费逻辑Account::withdraw(amount + fee_); }
};CheckingAccount acc;
acc.withdraw(100);   // ✅ 正确扣除手续费Account* pa = &acc;
pa->withdraw(100);   // ❌ 未扣除手续费!资金错误

解决方案1:改为虚函数(需保证派生类行为兼容)

class Account {
public:virtual void withdraw(double amount) {// 基础验证逻辑if (amount > balance_) throw InsufficientFunds();balance_ -= amount;}
};class CheckingAccount : public Account {
public:void withdraw(double amount) override {// 添加额外逻辑Account::withdraw(amount + fee_); }
};

解决方案2:非虚接口模式(NVI)

class Account {
public:void withdraw(double amount) { // 非虚公开接口// 通用前置条件检查validate(amount); doWithdraw(amount);postTransaction();}
private:virtual void doWithdraw(double amount) = 0; // 实现可定制
};class CheckingAccount : public Account {
private:void doWithdraw(double amount) override {// 实现手续费逻辑actualWithdraw(amount + fee_);}
};

⚖️ 3. 设计决策指南
场景正确做法错误做法
通用不变行为✅ 基类非虚函数⛔ 派生类重定义
可定制的核心行为✅ 虚函数(public或private)⛔ 重定义非虚函数
添加派生类特有功能✅ 新命名函数⛔ 重定义基类非虚函数
强化前置条件/弱化后置条件⚠️ 避免(违反LSP)⛔ 绝对禁止

现代C++防护机制

class Base {
public:// C++11:显式禁止覆盖void stableAPI() final; // 非虚函数也可用final// C++20:契约强化void criticalOperation(int x) [[expects: x > 0]] [[ensures: resourceAllocated()]]{// 实现}
};class Derived : public Base {
public:void stableAPI(); // 错误:final函数不可重定义void criticalOperation(int x) { // 错误:无法弱化前置条件// 必须满足 x>0 且确保资源分配}
};

💡 关键设计原则

  1. 静态绑定与动态绑定

    • 非虚函数:静态绑定(编译期根据声明类型确定)
    • 虚函数:动态绑定(运行期根据对象类型确定)
    Derived d;
    Base* pb = &d;pb->nonVirtual(); // 调用Base::nonVirtual(静态)
    pb->virtualFunc(); // 调用Derived::virtualFunc(动态)
    
  2. 名称隐藏规则

    class Base {
    public:void func(int);
    };class Derived : public Base {
    public:void func(double); // 隐藏Base::func(int)!
    };Derived d;
    d.func(42); // 调用Derived::func(double)// 需要static_cast<Base>(d).func(42)访问基类版本
    
  3. 模板方法模式应用

    class Document {
    public:void save() const { // 非虚模板方法checkPermissions();doSave();      // 虚函数钩子updateLog();}
    private:virtual void doSave() const = 0; // 派生类实现
    };class PdfDocument : public Document {
    private:void doSave() const override { // 实现定制保存// PDF专用保存逻辑}
    };// 所有文档类型统一调用接口
    PdfDocument doc;
    doc.save(); // 正确执行完整流程
    

危险模式重现

class Rectangle {
public:void setSize(int w, int h) {width_ = w;height_ = h;}
};class Square : public Rectangle {
public:void setSize(int s) { // 错误:重定义非虚函数width_ = height_ = s;}// 隐藏Rectangle::setSize(int,int)!
};Square s;
s.setSize(10);      // ✅ 调用Square版本
s.Rectangle::setSize(10, 20); // ✅ 显式调用(但破坏正方形不变式)Rectangle& r = s;
r.setSize(10, 20); // ❌ 编译通过,但破坏正方形不变式

安全重构方案

class Shape {
public:virtual void setSize(int w, int h) = 0;
};class Rectangle : public Shape {
public:void setSize(int w, int h) override { ... }
};class Square : public Shape {
public:void setSize(int w, int h) override {if (w != h) throw NotSquareException();size_ = w;}void setSize(int s) { // 重载(非重定义)size_ = s;}
};

C++17编译时检查

template<typename Derived>
class NonVirtualEnforcer {
public:~NonVirtualEnforcer() {static_assert(!std::is_same_v<decltype(&Derived::criticalFunc), decltype(&NonVirtualEnforcer::criticalFunc)>,"Do not redefine non-virtual functions!");}void criticalFunc();
};class Base : public NonVirtualEnforcer<Base> {};class Derived : public Base {
public:void criticalFunc(); // 触发static_assert错误
};
http://www.lryc.cn/news/617687.html

相关文章:

  • Excel 连接阿里云 RDS MySQL
  • 开闭原则代码示例
  • Pytest项目_day11(fixture、conftest)
  • js数组reduce高阶应用
  • B 树与 B + 树解析与实现
  • 可商用的 AI 图片生成工具推荐(2025 最新整理)
  • Kubernetes部署apisix的理论与最佳实践(一)
  • 专题:2025人形机器人与服务机器人技术及市场报告|附130+份报告PDF汇总下载
  • docker安装Engine stopped
  • 内置redis使用方法
  • Python 高阶函数:filter、map、reduce 详解
  • 【软考架构】主流数据持久化技术框架
  • Spring Boot Excel数据导入数据库实现详解
  • 6s081实验1
  • 机器翻译:一文掌握序列到序列(Seq2Seq)模型(包括手写Seq2Seq模型)
  • 机器学习TF-IDF算法详解
  • GPT-oss:OpenAI再次开源新模型,技术报告解读
  • 视频播放器哪个好用?视频播放器PotPlayer,KMP Player
  • FPGA学习笔记——DS18B20(数字温度传感器)
  • Mysql系列--6、内置函数
  • C++的结构体传参
  • 深度学习与遥感入门(五)|GAT 构图消融 + 分块全图预测:更稳更快的高光谱图分类(PyTorch Geometric 实战)
  • rust编译过程的中间表现形式如何查看,ast,hir,mir
  • Rust 实战五 | 配置 Tauri 应用图标及解决 exe 被识别为威胁的问题
  • istio如何采集method、url指标
  • Rust:anyhow 高效错误处理库核心用法详解
  • Elasticsearch 官方 Node.js 从零到生产
  • 用 Node.js 玩转 Elasticsearch从安装到增删改查
  • 基于动态顺序表实现【通讯录系统】:有文件操作部分哦!
  • 用 Docker 安装并启动 Redis:从入门到实战