学完Efficient c++ (34-35)

27 篇文章 0 订阅
24 篇文章 0 订阅
本文详细阐述了公共继承中纯虚函数、虚函数和非虚函数的区别,介绍了非虚接口(NVI)和策略模式的实现方法,如使用函数指针、std::function以及古典Strategy模式,强调了在不同继承机制下的设计决策和灵活性。
摘要由CSDN通过智能技术生成

条款34:区分接口继承和实现继承

我们在条款32讨论了public继承的实际意义,我们在本条款将明确在public继承体系中,不同类型的接口——纯虚函数、虚函数和非虚函数——背后隐藏的设计逻辑

首先需要明确的是,成员函数的接口总是会被继承,而public继承保证了,如果某个函数可施加在父类上,那么他一定能够被施加在子类上。不同类型的函数代表了父类对子类实现过程中不同的期望

  • 在父类中声明纯虚函数(不能创建父类的实体,只能创建其子类的实体),是为了强制子类拥有一个接口,并强制子类提供一份实现
  • 在父类中声明虚函数,是为了强制子类拥有一个接口,并为其提供一份缺省实现
  • 在父类中声明非虚函数,是为了强制子类拥有一个接口以及规定好的实现,并不允许子类对其做任何更改(条款36要求我们不得覆写父类的非虚函数)。

将纯虚函数、虚函数区分开的并不是在父类有没有实现——纯虚函数也可以有实现,其二者本质区别在于父类对子类的要求不同,前者在于从编译层面提醒子类主动实现接口,后者则侧重于给予子类自由度对接口做个性化适配。非虚函数则没有给予子类任何自由度,而是要求子类坚定的遵循父类的意志,保证所有继承体系内能有其一份实现

用非纯虚函数提供缺省的默认实现:

class Airplane {
public:
    virtual void Fly() {
        // 缺省实现
    }
};

class Model : public Airplane { ... };

这是最简朴的做法,但是这样做会带来的问题是,由于不强制对虚函数的覆写,在定义新的派生类时可能会忘记进行覆写,导致错误地使用了缺省实现

使用纯虚函数并提供默认实现:

class Airplane {
public:
    virtual void Fly() = 0;
protected:
    void DefaultFly() {
        // 缺省实现
    }
};

class Model : public Airplane { 
public:
    virtual void Fly() override {
        DefaultFly();
    }
};

上述写法可以替代为:

class Airplane {
public:
    virtual void Fly() = 0;
};

void Airplane::Fly() {
        // 缺省实现
}

class Model : public Airplane { 
public:
    virtual void Fly() override {
        Airplane::Fly();
    }
};

条款 35:考虑虚函数以外的其它选择

藉由非虚接口手法实现 template method:

非虚接口(non-virtual interface,NVI) 设计手法的核心就是用一个非虚函数作为 wrapper,将虚函数隐藏在封装之下:

class GameCharacter {
public:
    int HealthValue() const {
        ...    // 做一些前置工作
        int retVal = DoHealthValue();
        ...    // 做一些后置工作
        return retVal;
    }
    ...
private:
    virtual int DoHealthValue() const {
        ...    // 缺省算法
    }
};

NVI手法的一个优点就是在 wrapper 中做一些前置和后置工作,确保得以在一个虚函数被调用之前设定好适当场景(锁定互斥锁、制造运转日志、验证class约束条件等等),并在调用结束之后清理场景(互斥锁解除锁定、验证函数的事后条件、再次验证class约束条件等等)。如果你让客户直接调用虚函数,就没有任何好办法可以做这些事。

NVI手法允许派生类重新定义虚函数,从而赋予它们“如何实现机能”的控制能力,但基类保留“函数何时被调用”的权利。

在NVI手法中虚函数除了可以是private,也可以是protected,例如要求在派生类的虚函数实现内调用其基类的对应虚函数时,就必须得这么做。

藉由函数指针实现 Strategy 模式:

参考以下例子:

class GameCharacter;
int DefaultHealthCalc(const GameCharacter&);        // 缺省算法
class GameCharacter {
public:
    using HealthCalcFunc = int(*)(const GameCharacter&);    // 定义函数指针类型
    explicit GameCharacter(HealthCalcFunc hcf = DefaultHealthCalc)
        : healthFunc(hcf) {}
    int HealthValue() const { return healthFunc(*this); }
    ...
private:
    HealthCalcFunc healthFunc;
};

同一个人物类型的不同实体可以有不同的健康计算函数,并且该计算函数可以在运行期变更。

这间接表明健康计算函数不再是GameCharacter继承体系内的成员函数,它也无权使用非public成员。为了填补这个缺陷,我们唯一的做法是弱化类的封装,引入友元或提供public访问函数。

藉由 std::function 完成 Strategy 模式

std::function是 C++11 中引入的函数包装器,使用它能提供比函数指针更强的灵活度:

class GameCharacter;
int DefaultHealthCalc(const GameCharacter&);        // 缺省算法
class GameCharacter {
public:
    using HealthCalcFunc = std::function<int(const GameCharacter&)>;    // 定义函数包装器类型
    explicit GameCharacter(HealthCalcFunc hcf = DefaultHealthCalc)
        : healthFunc(hcf) {}
    int HealthValue() const { return healthFunc(*this); }
    ...
private:
    HealthCalcFunc healthFunc;
};

看起来并没有很大的改变,但当我们需要时,std::function就能展现出惊人的弹性:

// 使用返回值不同的函数
short CalcHealth(const GameCharacter&);

GameCharacter chara1(CalcHealth);

// 使用函数对象(仿函数)
struct HealthCalculator {
    int operator()(const GameCharacter&) const { ... }
};

GameCharacter chara2(HealthCalculator());

// 使用某个成员函数
class GameLevel {
public:
    float Health(const GameCharacter&) const;
    ...
};

GameLevel currentLevel;
GameCharacter chara3(std::bind(&GameLevel::Health, currentLevel, std::placeholders::_1));

为什么std::placeholders::_1意味“当为chara3调用GameLevel::Health时是以currentLevel作为GameLevel对象”。

古典的 Strategy 模式:

在古典的 Strategy 模式中,我们并非直接利用函数指针(或包装器)调用函数,而是内含一个指针指向来自HealthCalcFunc继承体系的对象:

class GameCharacter;

class HealthCalcFunc {
public:
    virtual int Calc(const GameCharacter& gc) const { ... }
    ...
};

HealthCalcFunc defaultHealthCalc;

class GameCharacter {
public:
    explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
        : pHealthCalc(phcf) {}
    int HealthValue() const { return pHealthCalc->Calc(*this); }
    ...
private:
    HealthCalcFunc* pHealthCalc;
};

这个设计模式的好处在于足够容易辨认,想要添加新的计算函数也只需要为HealthCalcFunc基类添加一个派生类即可。

  • 14
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值