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"!静态绑定
问题分析:
- 破坏多态:通过基类指针/引用调用时,总是调用基类版本
- 违反LSP:派生类对象无法替换基类对象(行为改变)
- 代码脆弱:
- 函数隐藏:
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 且确保资源分配}
};
💡 关键设计原则
-
静态绑定与动态绑定
- 非虚函数:静态绑定(编译期根据声明类型确定)
- 虚函数:动态绑定(运行期根据对象类型确定)
Derived d; Base* pb = &d;pb->nonVirtual(); // 调用Base::nonVirtual(静态) pb->virtualFunc(); // 调用Derived::virtualFunc(动态)
-
名称隐藏规则
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)访问基类版本
-
模板方法模式应用
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错误 };