Effective C++ 条款35:考虑 virtual 函数以外的其他选择
当你本能地想要使用 virtual 函数来实现多态行为时,请先停下来想一想:
这真的是最好的选择吗?本条款将为你打开四扇新的大门。
一、问题的提出:Virtual 函数是万能药吗?
假设你在设计一个游戏角色系统,每个角色都有一个计算健康值的方法:
cpp
class GameCharacter {
public:
virtual int healthValue() const {
// 默认实现
return baseHealth_ + level_ * 10;
}
virtual ~GameCharacter() = default;
protected:
int baseHealth_ = 100;
int level_ = 1;
};
class Warrior : public GameCharacter {
public:
int healthValue() const override {
// 战士:力量加成
return baseHealth_ + level_ * 15 + strength_ * 2;
}
private:
int strength_ = 20;
};
class Mage : public GameCharacter {
public:
int healthValue() const override {
// 法师:智力加成,但基础较低
return baseHealth_ * 0.8 + level_ * 8 + intelligence_ * 3;
}
private:
int intelligence_ = 25;
};
这是典型的面向对象设计:基类定义接口,派生类覆盖实现。但 Scott Meyers 在本条款中提醒我们:virtual 函数并非唯一选择,在某些场景下,替代方案可能更优。
二、替代方案一:NVI(Non-Virtual Interface)手法
2.1 什么是 NVI?
NVI (Non-Virtual Interface)是 Template Method 设计模式的一种特殊形式。其核心思想是:
将 virtual 函数声明为 private 或 protected,然后用一个 public 的 non-virtual 函数来包装它。
cpp
class GameCharacter {
public:
// Public non-virtual 接口:所有客户都调用这个函数
int healthValue() const {
// 1. 前置处理:可以做日志、校验、锁等
logHealthCheck();
// 2. 调用真正的实现(virtual 函数)
int ret = doHealthValue();
// 3. 后置处理:可以做缓存、断言等
assert(ret >= 0);
return ret;
}
virtual ~GameCharacter() = default;
protected:
int baseHealth_ = 100;
int level_ = 1;
private:
// Virtual 函数变为 private/protected,派生类覆盖它
virtual int doHealthValue() const {
return baseHealth_ + level_ * 10;
}
void logHealthCheck() const {
std::cout << "[LOG] 检查角色健康值\n";
}
};
class Warrior : public GameCharacter {
private:
int doHealthValue() const override {
return baseHealth_ + level_ * 15 + strength_ * 2;
}
int strength_ = 20;
};
class Mage : public GameCharacter {
private:
int doHealthValue() const override {
return baseHealth_ * 0.8 + level_ * 8 + intelligence_ * 3;
}
int intelligence_ = 25;
};
2.2 NVI 的优势
| 优势 | 说明 |
|---|---|
| 前置/后置条件 | 在 virtual 函数调用前后插入固定逻辑 |
| 接口稳定 | public 接口永远不会被覆盖,行为一致 |
| 实现灵活 | 派生类只关注核心算法,不关心周边逻辑 |
| 调试友好 | 可以在包装函数中统一添加日志、计时等 |
2.3 更实际的例子:带缓存的计算
cpp
class DataProcessor {
public:
// 客户只调用这个接口
double process(const std::vector<double>& data) {
// 前置:检查缓存
if (cacheValid_ && data == cachedInput_) {
std::cout << "[Cache Hit] 返回缓存结果\n";
return cachedResult_;
}
// 核心计算(由派生类实现)
double result = doProcess(data);
// 后置:更新缓存
cachedInput_ = data;
cachedResult_ = result;
cacheValid_ = true;
return result;
}
void invalidateCache() { cacheValid_ = false; }
virtual ~DataProcessor() = default;
protected:
virtual double doProcess(const std::vector<double>& data) = 0;
private:
std::vector<double> cachedInput_;
double cachedResult_ = 0.0;
bool cacheValid_ = false;
};
class MovingAverageProcessor : public DataProcessor {
protected:
double doProcess(const std::vector<double>& data) override {
if (data.empty()) return 0.0;
double sum = std::accumulate(data.begin(), data.end(), 0.0);
return sum / data.size();
}
};
class StandardDeviationProcessor : public DataProcessor {
protected:
double doProcess(const std::vector<double>& data) override {
if (data.size() < 2) return 0.0;
double mean = std::accumulate(data.begin(), data.end(), 0.0) / data.size();
double variance = 0.0;
for (double x : data) {
variance += (x - mean) * (x - mean);
}
return std::sqrt(variance / (data.size() - 1));
}
};
三、替代方案二:函数指针成员变量
3.1 将 virtual 函数替换为函数指针
如果健康值的计算逻辑不需要访问类的内部状态(或只需要有限访问),可以用函数指针来实现:
cpp
// 计算健康值的函数类型
using HealthCalcFunc = int (*)(const GameCharacter&);
// 默认的健康值计算函数
int defaultHealthCalc(const GameCharacter& gc) {
return gc.getBaseHealth() + gc.getLevel() * 10;
}
// 战士的健康值计算
int warriorHealthCalc(const GameCharacter& gc) {
// 通过 public 接口访问需要的信息
return gc.getBaseHealth() + gc.getLevel() * 15;
}
class GameCharacter {
public:
// 构造函数允许指定计算函数,默认使用默认算法
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthCalcFunc_(hcf) {}
int healthValue() const {
return healthCalcFunc_(*this);
}
// 运行时更换计算策略
void setHealthCalcFunc(HealthCalcFunc hcf) {
healthCalcFunc_ = hcf;
}
int getBaseHealth() const { return baseHealth_; }
int getLevel() const { return level_; }
private:
HealthCalcFunc healthCalcFunc_;
int baseHealth_ = 100;
int level_ = 1;
};
// 使用
int main() {
GameCharacter npc; // 使用默认计算
std::cout << "NPC 健康值: " << npc.healthValue() << "\n";
GameCharacter warrior(warriorHealthCalc); // 使用战士计算
std::cout << "战士健康值: " << warrior.healthValue() << "\n";
// 运行时切换策略!
warrior.setHealthCalcFunc(defaultHealthCalc);
std::cout << "战士(切换后)健康值: " << warrior.healthValue() << "\n";
}
3.2 函数指针的优缺点
| 优点 | 缺点 |
|---|---|
| 运行时动态切换行为 | 无法访问类的 private 成员 |
| 每个对象可以有不同的策略 | 函数签名固定,不够灵活 |
| 不需要继承体系 | 类型安全较弱 |
| 减少虚函数表开销 | 需要暴露更多内部状态(通过 public 接口) |
四、替代方案三:std::function 成员变量(推荐)
C++11 引入的 std::function 是函数指针的升级版,可以接受任何可调用对象。
4.1 使用 std::function
cpp
#include <functional>
#include <iostream>
class GameCharacter;
class GameCharacter {
public:
// std::function 可以接受:函数指针、lambda、bind 表达式、函数对象
using HealthCalcFunc = std::function<int(const GameCharacter&)>;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthCalcFunc_(hcf) {}
int healthValue() const {
return healthCalcFunc_(*this);
}
void setHealthCalcFunc(HealthCalcFunc hcf) {
healthCalcFunc_ = hcf;
}
int getBaseHealth() const { return baseHealth_; }
int getLevel() const { return level_; }
int getStrength() const { return strength_; }
private:
HealthCalcFunc healthCalcFunc_;
int baseHealth_ = 100;
int level_ = 1;
int strength_ = 20; // 为了演示,这里暴露给计算函数
};
// 默认计算
int defaultHealthCalc(const GameCharacter& gc) {
return gc.getBaseHealth() + gc.getLevel() * 10;
}
// 使用示例:展示 std::function 的强大灵活性
int main() {
// 1. 使用普通函数
GameCharacter npc1(defaultHealthCalc);
// 2. 使用 lambda(捕获外部变量!)
int bonus = 50;
GameCharacter npc2([&bonus](const GameCharacter& gc) {
return gc.getBaseHealth() + gc.getLevel() * 10 + bonus;
});
// 3. 使用 std::bind
auto calcWithMultiplier = std::bind(
[](const GameCharacter& gc, double mult) {
return static_cast<int>((gc.getBaseHealth() + gc.getLevel() * 10) * mult);
},
std::placeholders::_1, // 占位符:GameCharacter 参数
1.5 // 倍数
);
GameCharacter npc3(calcWithMultiplier);
// 4. 运行时动态更换为 lambda
GameCharacter warrior(defaultHealthCalc);
std::cout << "战士初始健康值: " << warrior.healthValue() << "\n";
// 装备了一件加生命的装备,更换计算方式
warrior.setHealthCalcFunc([](const GameCharacter& gc) {
return gc.getBaseHealth() + gc.getLevel() * 15 + gc.getStrength() * 2;
});
std::cout << "战士装备后健康值: " << warrior.healthValue() << "\n";
// 5. 使用成员函数(通过 bind)
class CustomCalculator {
public:
int calc(const GameCharacter& gc) {
return gc.getBaseHealth() * 2 + levelBonus_;
}
private:
int levelBonus_ = 100;
};
CustomCalculator calc;
GameCharacter npc4(std::bind(&CustomCalculator::calc, &calc, std::placeholders::_1));
}
4.2 std::function vs 函数指针
| 特性 | 函数指针 | std::function |
|---|---|---|
| 可接受 lambda | 否(无捕获的除外) | 是 |
| 可捕获外部变量 | 否 | 是 |
| 可绑定成员函数 | 困难 | 容易(通过 std::bind) |
| 性能 | 最优(直接调用) | 略有开销(类型擦除) |
| 类型安全 | 弱 | 强 |
| 灵活性 | 低 | 高 |
建议: 除非性能极其敏感,否则优先使用
std::function。它带来的灵活性远超微小的性能开销。
五、替代方案四:Strategy 设计模式(独立的继承体系)
5.1 将变化的行为提取到独立的类层次
这是 Strategy 设计模式的传统实现:将健康值计算逻辑提取到一个独立的继承体系中。
cpp
// 独立的计算策略继承体系
class HealthCalcStrategy {
public:
virtual ~HealthCalcStrategy() = default;
virtual int calc(const GameCharacter& gc) const = 0;
virtual std::string getName() const = 0;
};
// 默认策略
class DefaultHealthStrategy : public HealthCalcStrategy {
public:
int calc(const GameCharacter& gc) const override {
return gc.getBaseHealth() + gc.getLevel() * 10;
}
std::string getName() const override {
return "Default";
}
};
// 战士策略
class WarriorHealthStrategy : public HealthCalcStrategy {
public:
int calc(const GameCharacter& gc) const override {
return gc.getBaseHealth() + gc.getLevel() * 15 + gc.getStrength() * 2;
}
std::string getName() const override {
return "Warrior";
}
};
// 法师策略
class MageHealthStrategy : public HealthCalcStrategy {
public:
int calc(const GameCharacter& gc) const override {
return gc.getBaseHealth() * 0.8 + gc.getLevel() * 8 + gc.getIntelligence() * 3;
}
std::string getName() const override {
return "Mage";
}
};
// 可以动态组合的策略:装备加成
class EquipmentBonusStrategy : public HealthCalcStrategy {
public:
explicit EquipmentBonusStrategy(std::unique_ptr<HealthCalcStrategy> base,
int bonus)
: baseStrategy_(std::move(base)), bonus_(bonus) {}
int calc(const GameCharacter& gc) const override {
return baseStrategy_->calc(gc) + bonus_;
}
std::string getName() const override {
return baseStrategy_->getName() + " + Equipment(" + std::to_string(bonus_) + ")";
}
private:
std::unique_ptr<HealthCalcStrategy> baseStrategy_;
int bonus_;
};
// GameCharacter 类
class GameCharacter {
public:
explicit GameCharacter(std::unique_ptr<HealthCalcStrategy> strategy)
: healthStrategy_(std::move(strategy)) {}
int healthValue() const {
return healthStrategy_->calc(*this);
}
std::string getStrategyName() const {
return healthStrategy_->getName();
}
void setStrategy(std::unique_ptr<HealthCalcStrategy> strategy) {
healthStrategy_ = std::move(strategy);
}
// 提供策略需要的信息
int getBaseHealth() const { return baseHealth_; }
int getLevel() const { return level_; }
int getStrength() const { return strength_; }
int getIntelligence() const { return intelligence_; }
private:
std::unique_ptr<HealthCalcStrategy> healthStrategy_;
int baseHealth_ = 100;
int level_ = 1;
int strength_ = 20;
int intelligence_ = 25;
};
// 使用
int main() {
// 创建战士
GameCharacter warrior(std::make_unique<WarriorHealthStrategy>());
std::cout << "战士健康值: " << warrior.healthValue()
<< " (策略: " << warrior.getStrategyName() << ")\n";
// 给战士装备加生命值的装备
warrior.setStrategy(
std::make_unique<EquipmentBonusStrategy>(
std::make_unique<WarriorHealthStrategy>(),
50 // +50 生命值
)
);
std::cout << "战士(装备后)健康值: " << warrior.healthValue()
<< " (策略: " << warrior.getStrategyName() << ")\n";
// 创建法师
GameCharacter mage(std::make_unique<MageHealthStrategy>());
std::cout << "法师健康值: " << mage.healthValue()
<< " (策略: " << mage.getStrategyName() << ")\n";
}
5.2 Strategy 模式的优势
| 优势 | 说明 |
|---|---|
| 策略复用 | 同一种策略可以被多个角色使用 |
| 运行时组合 | 可以动态组合策略(如装备加成) |
| 独立演化 | 策略体系与角色体系独立发展 |
| 易于测试 | 策略可以独立单元测试 |
| 避免类爆炸 | 不需要为每种组合创建新的派生类 |
六、四种方案的对比与选择
cpp
// 方案对比:假设我们要实现一个"计算健康值"的功能
// ====== 方案1:传统 Virtual ======
class Character_Virtual {
virtual int healthValue() const; // 派生类覆盖
};
// ====== 方案2:NVI ======
class Character_NVI {
public:
int healthValue() const; // public 非虚,包含前置/后置逻辑
protected:
virtual int doHealthValue() const; // 派生类覆盖
};
// ====== 方案3:函数指针 ======
class Character_FuncPtr {
int (*healthCalc_)(const Character_FuncPtr&);
};
// ====== 方案4:std::function ======
class Character_Function {
std::function<int(const Character_Function&)> healthCalc_;
};
// ====== 方案5:Strategy 模式 ======
class Character_Strategy {
std::unique_ptr<HealthCalcStrategy> strategy_;
};
| 方案 | 灵活性 | 性能 | 封装性 | 适用场景 |
|---|---|---|---|---|
| 传统 Virtual | 中 | 中(虚函数开销) | 高 | 经典 OOP 场景,行为与类型强绑定 |
| NVI | 中 | 中 | 高 | 需要前置/后置处理的场景 |
| 函数指针 | 中 | 高 | 低 | 简单场景,性能敏感 |
| std::function | 高 | 略低 | 低 | 需要 lambda、运行时灵活切换 |
| Strategy 模式 | 很高 | 中(多一次间接) | 中 | 策略复杂、需要组合、独立演化 |
选择指南
需要实现运行时多态行为:
│
├─ 行为是否与对象类型强绑定?
│ ├─ 是 → 传统 Virtual 或 NVI
│ └─ 否 → 继续问:
│
├─ 是否需要前置/后置处理(日志、缓存、锁等)?
│ ├─ 是 → NVI
│ └─ 否 → 继续问:
│
├─ 策略是否简单且不需要访问私有状态?
│ ├─ 是 → 函数指针或 std::function
│ └─ 否 → 继续问:
│
├─ 策略是否复杂、需要独立演化、或被多个类共享?
│ ├─ 是 → Strategy 模式
│ └─ 否 → std::function(最灵活)
七、实际应用场景
场景1:游戏中的 Buff/Debuff 系统(Strategy + std::function)
cpp
class Buff {
public:
using EffectFunc = std::function<int(int baseValue, int duration)>;
Buff(std::string name, int duration, EffectFunc effect)
: name_(std::move(name)), duration_(duration), effect_(effect) {}
int apply(int baseValue) const {
return effect_(baseValue, duration_);
}
void tick() { --duration_; }
bool isExpired() const { return duration_ <= 0; }
std::string getName() const { return name_; }
private:
std::string name_;
int duration_;
EffectFunc effect_;
};
// 创建各种 buff
Buff healthBoost("生命提升", 10, [](int base, int dur) {
return base + 50; // +50 生命
});
Buff poison("中毒", 5, [](int base, int dur) {
return base - 10 * dur; // 每回合减血
});
Buff rage("狂暴", 3, [](int base, int dur) {
return static_cast<int>(base * 1.5); // 1.5 倍攻击
});
场景2:排序算法的动态选择(函数指针/std::function)
cpp
class Sorter {
public:
using CompareFunc = std::function<bool(int, int)>;
void setCompareStrategy(CompareFunc cmp) {
compare_ = cmp;
}
void sort(std::vector<int>& data) {
std::sort(data.begin(), data.end(), compare_);
}
private:
CompareFunc compare_ = std::less<int>{};
};
// 使用
Sorter sorter;
std::vector<int> data = {3, 1, 4, 1, 5, 9};
sorter.sort(data); // 升序
sorter.setCompareStrategy(std::greater<int>{});
sorter.sort(data); // 降序
sorter.setCompareStrategy([](int a, int b) {
return std::abs(a) < std::abs(b); // 按绝对值排序
});
sorter.sort(data);
场景3:网络请求的重试策略(NVI)
cpp
class NetworkClient {
public:
// NVI:统一的请求接口
Response sendRequest(const Request& req) {
int retries = 0;
while (retries < maxRetries_) {
try {
// 前置:记录日志
logRequest(req, retries);
// 核心:派生类实现实际发送
Response resp = doSendRequest(req);
// 后置:校验响应
if (validateResponse(resp)) {
return resp;
}
} catch (const NetworkException& e) {
logError(e, retries);
}
++retries;
if (retries < maxRetries_) {
waitBeforeRetry(retries);
}
}
throw MaxRetriesExceededException();
}
virtual ~NetworkClient() = default;
protected:
virtual Response doSendRequest(const Request& req) = 0;
virtual void waitBeforeRetry(int retryCount) {
std::this_thread::sleep_for(std::chrono::seconds(retryCount));
}
private:
int maxRetries_ = 3;
void logRequest(const Request& req, int retry) {
std::cout << "[Request] 重试次数: " << retry << "\n";
}
void logError(const NetworkException& e, int retry) {
std::cout << "[Error] " << e.what() << ",准备重试...\n";
}
bool validateResponse(const Response& resp) {
return resp.statusCode >= 200 && resp.statusCode < 300;
}
};
class HttpClient : public NetworkClient {
protected:
Response doSendRequest(const Request& req) override {
// 实现 HTTP 请求发送
return httpSend(req.url, req.body);
}
};
class GrpcClient : public NetworkClient {
protected:
Response doSendRequest(const Request& req) override {
// 实现 gRPC 请求发送
return grpcSend(req.service, req.method, req.body);
}
};
八、总结
| 替代方案 | 核心思想 | 最佳适用场景 |
|---|---|---|
| NVI | public non-virtual 包装 private/protected virtual | 需要统一的前置/后置处理 |
| 函数指针 | 用成员函数指针替代 virtual | 简单策略,性能敏感 |
| std::function | 用可调用对象替代 virtual | 需要 lambda,运行时灵活切换 |
| Strategy 模式 | 将策略提取到独立继承体系 | 策略复杂,需要组合和复用 |
请记住:
- virtual 函数的替代方案包括 NVI 手法及 Strategy 设计模式的多种形式。NVI 手法自身是一个特殊形式的 Template Method 设计模式。
- 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问 class 的 non-public 成员。
std::function对象的行为就像一般函数指针。这样的对象可接纳"与给定之目标签名式兼容"的所有可调用物。
Virtual 函数是 C++ 多态的基石,但它不是唯一的选择。根据具体场景选择最合适的设计方案,才能写出既灵活又高效的代码。
参考:《Effective C++》第三版,Scott Meyers 著
相关条款:条款34(区分接口继承和实现继承)、条款36(绝不重新定义继承而来的 non-virtual 函数)、条款37(绝不重新定义继承而来的缺省参数值)