C++多态实现

2023/03/27 cpp 共 2728 字,约 8 分钟

引言

C++多态分为静态多态和动态多态, 动态多态主要由继承和虚函数实现, 这一机制主要在运行期间起作用,因此我们称之为动态多态(dynamic polymorphism)。模板也允许我们用单个统一符号将不同的特定行为关联起来,不过该关联主要发生在编译期间,我们称之为静态多态(static polymorphism)。

动态多态

动态多态最常用的实现肯定是基于虚函数的机制,即在基类声明一个虚函数,在派生类中进行重写同名虚函数。基类的引用和指针指向一个派生类对象,当调用该函数的时候,就可以通过基类指针或者引用调用派生类函数。

动态多态的实现是基于虚函数表(Virtual Table)(指针数组,每一个指针指向一个函数地址)。即只要是有虚函数的类在内存中就有一个虚函数表,派生类会拷贝基类的虚函数表,重写的时候会覆盖该条目,然后添加新的虚函数会追加到虚函数表的最后。每个有虚函数的类的实例有一个虚函数指针,一般放在对象刚开始的位置。

虚函数的调用可以按照下面的例子来理解,ptr为对象指针,vptr表示由编译器产生的指针,指向虚函数表。offset为函数在虚函数表的偏移量。

ptr->vptr表示获取指针ptr所指向对象的虚函数表指针,offset是虚函数在虚函数表中的偏移量。*ptr->vptr[offset]表示获取虚函数表中偏移量为offset的虚函数指针,并使用解引用操作符*获取该虚函数指针所指向的虚函数。最后,将指针ptr作为参数传入该虚函数进行调用,语法形式为(ptr)

*ptr->vptr[offset])(ptr)

除了上面传统的方法,也可以用C++17的std::variant和std::visit以实现多态。

std::variant

​ 类模板 std::variant 表示一个类型安全的联合体(取代传统的union)。 std::variant 的一个实例在任意时刻要么保有其一个可选类型之一的值,要么在错误情况下无值。

​ 使用std::variant的基本方式是定义一个std::variant对象,指定它可以存储的所有类型,然后通过不同的方式来访问其中的值。例如:

int main() {
    std::variant<int, double, std::string> v = "hello";
    std::cout << std::get<std::string>(v) << std::endl;
    // 如果要获取的值类型与variant实际存储的类型不匹配,将会抛出std::bad_variant_access异常。
    return 0;
}

除了使用std::get,还可以使用std::visit进行访问

std::visit

​ std::visit的定义如下:

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

vis是一个访问器,可以是函数对象或者泛型lambda,而vars则是传给访问器的参数列表。换句话说,std::visit能将所有变体类型参数所存放的数据作为参数传给函数。最简单的使用访问器的方式是使用泛型lambda,它是一个可以处理任意类型的函数对象。

int main() {
  std::variant<int, std::string> v;
  v = "with Generic Lambdas";
  std::visit([](const auto &val) {
  std::cout << val << std::endl;}, v);
  
  return 0;
}

输出如下

with Generic Lambdas

std::variant 和 std::visit 实现动态多态

定义如下派生类和基类,其中func为非虚函数

class Base {
 public:
  void func() const {
    std::cout << "Base::func" << std::endl;
  }
};

class Derived : public Base {
 public:
  void func() const {
    std::cout << "Derived::func" << std::endl;
  }
};

然后就可以使用如下方法进行多态的模拟

int main() {
    std::variant<Base, Derived> v = Derived();
    std::visit([](auto&& d){ d.func();}, v);

    v = Base();
    std::visit([](auto&& d){ d.func();}, v);
}

输出如下:

Derived::func
Base::func

相比传统方案的优点

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

缺点

  • 需要在编译时预先了解所有类型
  • 浪费内存,因为std::variant大小是支持类型的最大大小。
  • 每个多态操作都需要实现一个对应的visit,使用泛型的时候也会对不同的类型实例化不同的visit代码

静态多态

静态多态是一种在编译期就确定的多态形式,其实现方式是通过模板来实现。在静态多态中,编译器会在编译期根据函数的参数类型和模板的实例化类型来确定具体的函数实现,因此其运行效率很高。模板中常用的静态多态模式之一是CRTP(Curiously Recurring Template Pattern,译为“奇异递归模板模式”)。

CRTP之前以及有类似的文章

静态多态 vs 动态多态

动态多态

  • 可以很优雅的处理异质集合。(比如使用一个基类指针放入vector中,这些基类指针可以指向不同的派生类)
  • 可执行文件的大小可能会比较小(因为它只需要一个多态函数,不像静态多态那样,需要为不同的类型进行各自的实例化)。
  • 代码可以被完整的编译;因此没有必须要被公开的代码(在发布模板库时通常需要发布模板的源代码实现)

静态多态

  • 产生的代码可能会更快(因为不需要通过指针进行重定向,先验的(priori)非虚函数通常也更容易被inline)。
  • 即使某个具体类型只提供了部分的接口,也可以用于静态多态,只要不会用到那些没有被实现的接口即可。

Reference

多态实现-虚函数、函数指针以及变体 (qq.com)

《C++ templates》

Search

    Table of Contents