Effective C++ 条款35:考虑 virtual 函数以外的其他选择

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(绝不重新定义继承而来的缺省参数值)

相关推荐
郝学胜-神的一滴1 小时前
CMake 017:彩色日志输出实战
linux·c语言·开发语言·c++·软件工程·软件构建·cmake
garmin Chen1 小时前
从 Transformer 到 Agent:大模型技术全景解析
java·人工智能·python·深度学习·transformer
愚公移码1 小时前
蓝凌EKP18产品:流程引擎技术篇之流程核心概念模型
java·人工智能·流程引擎·蓝凌
Full Stack Developme1 小时前
Apache Tika 教程
java·开发语言·python·apache
鹅城剑仙2 小时前
Java线程池完全指南
java
桀人2 小时前
C++——string类的详细介绍
开发语言·c++
李白的天不白2 小时前
SmartAdmin(基于 Spring Boot 框架)中配置跨域请求 VUE3 设置请求头
java·前端
橙子进阶之路2 小时前
Java线程(CompletableFuture)
java·开发语言
鹅城剑仙2 小时前
Java CompletableFuture 异步编程完全指南
java