Skip to main content

动态多态和静态多态

· 5 min read

动态多态

动态多态的优点是可以实现多质变量的容器,然后遍历执行同名函数,产生不同效果。 缺点显然就是vptr的寻址开销

namespace VirtualFunction {
struct IShape {
virtual ~IShape() = default;

virtual float Area() = 0;
};

struct Circle : public IShape {
float r = 1;

float Area() override {
return 3.14f * r * r;
}
};

struct Rect : public IShape {
float a = 1, b = 1;

float Area() override {
return a * b;
}
};

void f() {
IShape *circle = new Circle();
IShape *rect = new Rect();
std::cout << circle->Area() << rect->Area();
delete circle;
delete rect;
}
}

静态多态

模版函数

用模版函数,把抽象类作为模版参数传入,然后执行,虽然表达上能体现多态性, 但是单纯这样似乎没有存在的意义。毕竟在使用多态时,不会一个个手动的去创建对象执行函数。

因此优点是编译器多态,没有运行时额外开销;缺点是做不到“多质变量容器遍历”【除非通过variant,后文会讲】

namespace NormalTemplate {
struct Circle {
float r = 1;
float Area() const { return 3.14f * r * r; }
};

struct Rect {
float a = 1, b = 1;
float Area() const { return a * b; }
};

template<typename Shape>
float Area(const Shape &shape) {
return shape.Area();
}

void f(){
Circle circle;
Rect rect;
std::cout << Area(circle) << Area(rect);
}
}

CRTP

普通的模版函数的实现能表达多态语义,但是存在没有更多的合理性; 而CRTP除了能表达多态语义(is-a),在本质上它是一种has-ability的语义.

is-a就是熟知的继承关系,Circle继承自Shape,自然有Shape计算Area的功能; 而多个CRTP Derived类表达的是这些类都有一些共同的能力,这个能力不是从Base那边继承过来的(虽然好像听起来和多态语义也差不多)。

在应用中,CRTP存在的意义就是简化代码,而不是异质容器遍历。

比如我有很多卡牌,都希望它有复制自身属性的能力

struct Card{}
struct CardA : public Card{
CardA Clone(){
return CardA(*this);
}
int a;
}

struct CardB : public Card{
CardB Clone(){
return CardB(*this);
}
float b;
}

如果这样写,每多一个卡牌都要重新写一下Clone函数,此时就可以通过CRTP,表达卡牌拥有复制自身到能力,来实现多个类有相似函数的简化写法

template<typename Derived>
struct Card {
Derived Clone() {
return Derived(*static_cast<Derived *>(this));
};
};

struct CardA : public Card<CardA> {
int a = 1;
};

struct CardB : public Card<CardA> {
float b = 1;
};

void f(){
CardA a;
auto a1 = a.Clone();
}

这种方式非常常见,各种开源项目中都有出现,比如UE的TCommands

再来一个体现CRTP多态语义的例子

namespace CRTP {
template<typename Derived>
struct IShape {
float Area() {
return dynamic_cast<Derived *>(this)->Area();
}
};

struct Circle : public IShape<Circle> {
float r = 1;

float Area() {
return 3.14f * r * r;
}
};

struct Rect : public IShape<Rect> {
float a = 1, b = 1;

float Area() {
return a * b;
}
};

void f() {
Circle circle;
Rect rect;
std::cout << circle.Area() << rect.Area();
}
}

同样的,这样的多态也是提供不了异质容器遍历

用Variant改造静态多态

通过Variant,就可以强行赋予两种静态多态异质容器遍历, 虽然好像没见哪个开源软件这样写过。

namespace NormalVariant {
struct Circle {
float r = 1;

float Area() const {
return 3.14f * r * r;
}
};

struct Rect {
float a = 1, b = 1;

float Area() const {
return a * b;
}
};

template<typename Shape>
float Area(const Shape &shape) {
return shape.Area();
}

void f() {
std::variant<Circle, Rect> shape;
std::vector<std::variant<Circle, Rect>> ShapeList;
for (const auto &item: ShapeList) {
std::visit([](auto &&arg) {
std::cout << Area(arg);
}, item);
}
}
}

思考一下这有什么缺点导致没什么人用(猜测)

  1. variant保存data指针和额外的一个int Index指示当前存储的类型, 内存开销更大
  2. visit写法让人分解,仿函数也不便于debug