多态

多态字面理解就是有多种形式和形态,编程中专门指一种可以将不同的行为关联到一个泛型符号的能力。
多态是面向对象编程的基石之一,C++主要通过类的继承和虚函数来实现多态。
多态又可以分为动多态和静多态,主要的区别是多态的表现形式是在运行期处理,还是在编译期处理。

动多态#

动多态是在运行期处理多态行为,常常说的“多态”也大多指这种形式。
多态的设计思想在于:识别相关对象类型中的一组公共功能,并将其声明为公共基类中的虚函数接口。

struct Coord {
    int x = 0;
    int y = 0;

    Coord operator-(const Coord& other) {
        return { x - other.x, y - other.y };
    }

    Coord abs() {
        return { std::abs(x), std::abs(y) };
    }
};

class GeoObj {
public:
    virtual void draw() const = 0;

    virtual Coord centerOfOravity() const = 0;

    virtual ~GeoObj() = default;
};

使用类的多态,需要将一些公共接口用虚基类抽象出来,比如上述抽象出了一个图形对象的虚基类,有两个方法,draw()会画出该图形的形状,而centerOfGravity()会返回图形的重心位置。
我们将继承这个虚基类,让各种图形实现自己对应的公共方法。

class Circle : public GeoObj {
public:
    void draw() const override;

    Coord centerOfGravity() const override;
};

class Line : public GeoObj {
public:
    void draw() const override;

    Coord centerOfGravity() const override;
};

class Rectangle : public GeoObj {
public:
    void draw() const override;

    Coord centerOfGravity() const override;
};

有三个图形继承了上面的虚基类,分别是圆、直线和矩形,不妨假设它们都已经实现了对应的公共虚函数。然后我们会通过多态的方式使用它们。

void myDraw(const GeoObj& obj) {
    obj.draw();
}

Coord distance(const GeoObj& x1, const GeoObj& x2) {
    Coord c = x1.centerOfGravity() - x2.centerOfGravity();
    return c.abs();
}

void drawElements(const std::vector<GeoObj*>& elems) {
    for(auto elem : elems) {
        elem->draw();
    }
}

int main() {
    Line l;
    Circle c1, c2;

    myDraw(l);
    myDraw(c1);

    distance(c1, c2);
    distance(l, c1);

    std::vector<GeoObj*> coll;
    coll.push_back(&l);
    coll.push_back(&c1);

    return 0;
}

我们又定义了几个函数,分别用来画出形状,计算图形重心间的距离和批量画出数组中的所有图形的形状。
可以看到,画出单个图形形状和计算重心距离的函数使用了虚基类GeoObj的引用作为入参,批量绘制图形形状的接口使用虚基类的指针作为动态数组vector的元素类型。
在main函数中,实例化了一个直线图形和两个圆的对象,它们可以直接作为我们定义的函数的入参,这就是多态。通过虚基类的引用或指针,可以访问派生类中重新实现的方法,而这些方法会根据实现的不同,表现出不同的行为。
多态最优秀的特点就是可以批量地处理异类对象集合的能力。

静多态#

模板也可以实现多态,而且模板并不依赖包含公共行为的虚基类,只需要调用的对象有着同名的函数即可。

template<typename GeoObj>
void myDraw(const GeoObj& obj) {
    obj.draw();
}

使用这个模板函数,图形不再需要继承虚基类GeoObj,只需要在各自内部有名为draw的成员函数即可。
换句话说,模板实际上天然具有了多态的特性,它会在编译期根据指定的模板参数类型去调用恰当的函数,所以也被叫做静多态。
不过,静多态不再能够处理void drawElements(const std::vector<GeoObj*>& elems),因为模板参数必须通过某种方式显式地指定,就不再能够处理异类对象集合这种情况。这是静多态静态特性所施加的约束,换取的是性能和类型安全方面的一些优势。

总结#

  • 通过继承实现的多态是绑定和动态的:

绑定: 意味着参与多态行为的类型的接口是通过继承公共基类来获取的,而这个公共基类是事先设计好的。
动态: 意味着接口的绑定是在运行期(动态地)完成的。

  • 通过模板实现的多态是非绑定的和静态的:

非绑定: 意味着参与多态行为的类型的接口并不是预先确定的。
静态:意味着接口的绑定是在编译期(静态地)完成的。

优缺点#

动多态具有的优点:

  1. 优雅地处理异类集合。
  2. 可执行代码的大小可能更小。
  3. 代码可以完全编译,不必发布任何实现源码(分发模板库通常需要分发模板实现的源码)。

静多态具有的优点:

  1. 内置类型的集合容易实现(不必继承公共基类)。
  2. 生成的代码效率更高(因为不需要通过指针来调用,可以更频繁地内联非虚函数)。
  3. 如果程序仅需要执行部分接口,仍可以只提供对应的部分接口。

动多态与静多态并非是非此即彼的关系,实际开发中往往是结合着使用的。通过合理地使用两者,可以得到一些更灵活和强大的代码实现。

使用概念#

静多态强大之处也往往是让人诟病的一点,更高的灵活性意味着没有公共的接口类设计,这样可能会造成一些难以理解的情况发生,甚至是一些能通过编译但是完全不符合预期的行为。
也就是说,模板太“无法无天”了,我们亟需一些限制来约束模板的行为。为此,C++17标准提出了概念(concept)关键字(C++20中已经成为了标准的一部分)。

template<typename T>
concept GeoObj = requires(T x) {
    { x.draw() } ->void;
    { x.centerOfGravity() } ->Coord;
};

这里使用关键字concept定义了GeoObj的概念,它将模板参数类型约束为具有适当返回类型的成员函数drawcenterOfGravity的类型。
于是我们可以使用这个概念来约束模板参数:

template<typename T>
requires GeoObj<T>
void myDraw(const T& obj) {
    obj.draw();
}

template<typename T1, typename T2>
requires GeoObj<T1> && GeoObj<T2>
void distance(const T1& x1, const T2& x2) {
    Coord c = x1.centerOfGravity() - x2.centerOfGravity();
    return c.abs();    
}

在实例化模板时,会判断模板参数在概念约束下的执行结果,只有通过了概念的约束,判断为true时才会被实例化。

新形式的设计模式#

传统的桥接模式:

桥接模式

传统的桥接模式通过继承来实现:在接口类中定义一个公共基类的指针,通过动多态在多个不同的实现之间进行切换。这就需要提前设计好一个公共虚基类,而采用模板的的方式来实现时,就可以跳过设计公共基类的过程。

模板实现的桥接模式

作者:cwtxx

出处:https://www.cnblogs.com/cwtxx/p/18718192

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   cwtxx  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示