类型与对象(四)

1.6 运行时多态

  在软件开发中往往面临着大量选择的问题,不同的编程范式拥有不同的解决方案:面向过程编程范式采用大量的if-else、switch-case做“选择”,往往面临着将 “选择” 这个细节散布到代码各处的问题;面向对象编程范式采用接口类将 “选择” 这个细节屏蔽于工厂中;函数式编程范式采用模式匹配做 “选择”。

  选择问题往往是软件复杂的原因所在,因此我们需要很好的手段来隔离这些细节:即依赖抽象而不是细节,依赖统一的概念。这种处理问题的思路被称为多态:同一外表之下的多种形态。

>> 1.6.1 运行时多态手段

  C++语言最初作为一门面向对象编程语言,它提供的唯一运行时多态特性即虚函数机制。C++进行面向对象编程涉及的概念有抽象类、具体类与对象,抽象类充当接口的作用,它包含大量虚函数,用于应对变化、隔离细节;具体类用于实例化运行时的对象,它通过继承方式实现抽象类所表达的接口;而对象是最终整个系统运行的数据载体,对象之间通过接口交互实现整个系统的业务逻辑。继承的方式不仅可以用于表达实现语义,同样也能表达组合语义、复用代码。C++面向对象编程范式典型uml图如下。

   面向对象编程范式使用广泛,积累了很多可复用的模式:面向对象设计模式。从图中可推导出常见的设计模式。

  * 将AbstractClass替换成State,methood替换成handle,ConcreteClass替换成ConcreteState,便得到状态模式,用于建模业务的状态变迁。

  * 将AbstractClass替换成Command,methood替换成execute,ConcreteClass替换成ConcreteCommand便得到命令模式,C++标准库提供的std::function与lambda特性即命令模式,因而实际中无需再使用这个模式,从而减少多余的接口。

  * 将AbstractClass替换成AbsteactExpression,methood替换成interpet,ConcreteClass替换成TerminalExpression与NonTerminalExpression,便得到解释器模式,用于实现简单的DSL。

  * 将AbstractClass替换成Component,methood替换成operation,ConcreteClass替换成Leaf与Composite,便得到组合模式,从而形成树结构,例如文件目录树。

  * 将AbstractClass替换成Target,methood替换成operation,ConcreteClass替换成Adapter,便得到适配器模式,它将一个已有的接口适配成目标接口。

  * 将AbstractClass替换成Strategy,methood替换成algorithm,ConcreteClass替换成ConcreteStrategy,便得到策略模式,使其能够在运行时选择不同策略。

  还有很多设计模式就不在这里一 一罗列了,读者可以根据基本概念,推导出剩下的设计模式,这也说明了设计模式并不需要生搬硬套,读者只需要在不同的场景设计合适的接口,寻找合适的对象。简而言之,面向对象注重的是对象封装性,通过统一的抽象接口隔离不同的实现细节,从而分离变化方向,这也是依赖倒置的思想。

  C++的运行时多态机制采用继承方式,它往往伴随着指针、引用,他们都能实现引用(指针)语义多态,统一的接口类指针、引用拥有不同的实现,并且往往涉及动态内存分配,而C++17标准库的std::variant出现配合std::visit,与指针、引用方式相比,能实现值语义多态。

  Scott Meyers建议设计一个像int一样的基础数据类型,因为使用基础数据类型非常方便,可以在堆、栈上创建值,通过值传递,轻松通过拷贝语义创建一个完全独立的副本,或者通过移动语义移动一个值,这些都是值语义具备的特性。

  需要注意的是,不能简单的通过obj->methood()表现形式来判断obj是指针语义还是值语义。例如std::unique_ptr实现为指针语义,operator->返回持有指针;而std::optional实现为值语义,operator->返回持有值。同样的,也不能简单的通过是否需要动态内存分配来区分指针语义还是值语义。

  接下来我们看一个经典的面向对象设计中的例子,对形状进行操作,这里将采用引用语义多态与值语义多态进行对比,并给出他们各自的优缺点。

  首先是面向对象的引用语义多态方式,该方式提供统一的接口Shape隔离不同的形状,对统一接口进行操作获取不同的形状结果。

constexpr auto M_PI = 3.14159265355;

struct Shape {
    virtual ~Shape() = default;
    virtual double getArea() const = 0;
    virtual double getPerimeter() const = 0;
};

struct Circle : Shape {
    Circle(double r):r_(r){}
    double getArea() const override{
        return M_PI * r_ * r_;
    }
    double getPerimeter()const override{
        return 2 * M_PI * r_;
    }
private:
    double r_;
};

struct Rectangle : Shape {
    Rectangle(double w, double h):w_(w),h_(h){}
    double getArea() const override {
        return w_ * h_;
    }
    double getPerimeter()const override {
        return 2 * (w_ + h_);
    }
private:
    double w_;
    double h_;
};

int main()
{
    std::unique_ptr<Shape> shape = std::make_unique<Circle>(2);
    std::cout << "shape area" << shape->getArea() << "perimeter" << shape->getPerimeter() << std::endl;

    shape = std::make_unique<Rectangle>(2, 3);
    std::cout << "shape area" << shape->getArea() << "perimeter" << shape->getPerimeter() << std::endl;
}

  最后我们使用值语义的运行时多态方式,使用std::variant构造统一的形状Shape,通过统一的行为来操作不同的形状以获取对应的结果。

constexpr auto M_PI = 3.14159265355;

struct Circle {
    double r_;
};
double getArea(const Circle& c) {
    return M_PI * c.r_ * c.r_;
}
double getPerimeter(const Circle& c) {
    return 2 * c.r_ * c.r_;
}
//------------------------------------------------
struct Rectangle {
    double w_;
    double h_;
};
double getArea(const Rectangle& r) {
    return r.w_ * r.h_;
}
double getPerimeter(const Rectangle& r) {
    return 2 * (r.h_ + r.w_);
}
//----------------------------------------------------
using Shape = std::variant<Circle, Rectangle>;

double getArea(const Shape& s) {
    return std::visit([](const auto& data) {return getArea(data); }, s);
}
double getPerimeter(const Shape& s) {
    return std::visit([](const auto& data) {return getPerimeter(data); }, s);
}
int main()
{
    
    Shape shape = Circle{ 2 };
    std::cout << "shape area:" << getArea(shape) << "perimeter:" << getPerimeter(shape) << std::endl;

    shape = Rectangle{ 2,3 };
    std::cout << "shape area:" << getArea(shape) << "perimeter:" << getPerimeter(shape) << std::endl;

}

  这种多态方式的核心在于std::visit与std::vaiant类型的组合,需要注意的是visit接受一个泛型函数对象,背后的机制是经典的双重派发技术。

  * 通过元编程技术在编译期生成函数表,与此同时泛型函数对象的模板成员函数operator()将实例化并派发给实际的函数。

  * 运行时根据传递的variant索引查找表中的函数进行派发调用。

 

posted @ 2023-07-01 20:00  饼干`  阅读(9)  评论(0编辑  收藏  举报