【C++】动态多态

多态【Polymorphism】是面向对象程序设计的一个重要特性,即一个接口,多种实现方式。

在C++中,多态体现为两个方面,一个是编译时多态,一个是运行时多态。编译时多态是静态多态,运行时时动态多态。

静态多态分为【函数重载】和【模板(也称为泛型编程)】。

函数重载主要是C++ 将参数列表也作为函数的标识,不想C,只是将函数名作为标识。那么就存在最佳匹配问题。

运行时多态主要指【虚函数】

虚函数:
在基类声明一个虚函数,在子类中进行重写。基类的引用或者指针是指向派生类的对象的,基类调用该函数时,自动指向派生类的函数,这就是动态多态。

注意:如果不声明一个虚函数,那么就是【函数隐藏/覆盖】,注意这里是子类和父类拥有同名函数,而无需考虑参数列表。

具体实现
每个有虚函数的类都会有一个虚函数表,这里保存的就是虚函数的地址信息。所有存在虚函数的类的实例都会在内存中保存一个虚函数表,当调用虚函数的时候,从虚函数表中查找对应的需要调用的函数地址。

编译器会为每个存在虚函数的类对象插入一个vtpr(virtul function pointer),该vptr指向存放了虚函数地址的虚函数表vtbl,这样对象在调用虚函数的时候,第一步会先根据vptr找到vbtl,然后根据该虚函数在vbtl中的索引来进行调用,这样就实现了运行时多态功能。

大部分编译器的实现,都是将vptr放在对象的首位,所以我们可以通过这个特点来直接调用虚函数表中的函数。

Derived d;
long address = *(long*)&d;
Fun fun= (Fun)(*(long*)address);

对比普通成员函数和虚函数
在C++中,允许对函数进行重载,那么就需要对函数进行name mangling。

在此,说下编译器mangling后函数名的规则,仍然以成员函数Print()优化后的名称_ZN4Base5PrintEv为例(这个规则以笔者使用的gcc为例):

  • 编码后的符号由_Z开头
  • 如果有作用域符,则在_Z之后加上N
  • 接着是命名空间名字长度、命名空间名字、类名字长度、类名、成员函数名称、函数名称
  • 如果有作用域符,则以E结尾
  • 最后加上函数形参符号,void是v,int是i,char是c,P代表指针,有几个形参就写几个符号

从上述规则我们可以看出,C++中的重载只跟函数名和函数参数有关。

而对于成员函数,会将其转化成对应的非成员函数:
#1 安插一个额外的参数,const class *this 进入成员函数
#2 将成员函数进行mangling 处理,转换成独一无二的函数名
最终会被转化成一个普通的函数。

而虚函数对比普通的成员函数,多了一步虚函数寻址。
不过基类指针指向什么具体类型,但是总可以找到对应对象的vtbl,就可以进行函数访问。

C++17中引入了 variant 和 visit 以实现多态

variant是C++17引入的变体类型,它最大的优势是提供了一种新的具有多态性的处理不同类型集合的方法。也就是说,它可以帮助我们处理不同类型的数据,并且不需要公共基类和指针。

可以将其理解为union的升级版,之所以称之为升级版,是因为union有如下缺点:

  • 对象并不知道它们现在持有的值的类型
  • 不能持有std::string等非平凡类型
  • 不能被继承

既然称之为union的升级版,那么union的缺点其肯定不存在的,在此我们整理了下variant的特点:

  • 可以获取当前类型
  • 可以持有任何类型的值(不能是引用、C类型的数组指针、void等)
  • 可以被继承

【visit】
定义:

template <class Visitor, class... Variants>
constexpr visit( Visitor&& vis, Variants&&... vars );

在上述定义中,vis是一个访问器,而vars则是传给访问器的参数列表。换句话说,std::visit能将所有变体类型参数所存放的数据作为参数传给函数。

std::visit访问器可以是函数对象、泛型lambda以及重载的lambda等。

#include <iostream>
#include <string>
#include <variant>

struct Visitor {
  void operator()(int n) const {
    std::cout << "int: " << n << std::endl;
  }
  
  void operator()(const std::string &str) const {
    std::cout << "string: " << str << std::endl;
  }
};
  
int main() {
  std::variant<int, std::string> v;
  Visitor vst;
  v = "with Visitor";
  std::visit(vst, v);
  return 0;
}

输入如下:

string:with Visitor

结合variant 和 visit 实现多态

struct CallPrint {
    void operator()(const Base& b) { b.Print(); }    
    void operator()(const Derived& d) { d.Print(); }    
};

int main() {
  std::variant<Base, Derived> v = Derived();
  std::visit(CallPrint{}, v);
  v = Base();
  std::visit(CallPrint{}, v);
  return 0;
}

这就需要从其优缺点来进行分析,使用者可以根据其特点进行选择,首先,总结下其优点:

  • 值语义,无需动态分配
  • 不需要基类,类之间可以不相关
  • 相比于虚函数的重载(函数名、参数完全一致),variant只需要函数名一致即可,即不同的类里面可以函数名相同而参数不同,通过visit来进行对应的调用,从而实现多态

看完了前面的内容,其缺点也相对来说比较明显,如下:

  • 需要在编译时预先了解所有类型
  • 浪费内存,因为std::variant大小是支持类型的最大大小。因此,如果一种类型是 10 字节,另一种是100 字节,那么每个变体至少是 100 字节。因此,您可能会丢失 90 个字节
  • 每个多态操作都需要实现一个对应的visit
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值