C++中的多态

简介

让我们从一个简单的Demo开始。

1
2
3
4
5
6
Type1 x;
Type2 y;

//此处的f()代表了对给定的输入x、y执行的操作。
f(x);
f(y);

要体现多态,f()必须能够对至少两种不同的类型(e.g. int and double ),查找并执行不同的代码实现。

简单来说,多态为不同类型的对象提供了一个同一个接口,它是面向对象编程领域的一个常见概念。此外,封装可以使得代码模块化,继承可以扩展已存在的代码,它们的目的都是为了代码重用。

多态的目的则是为了“接口重用”。也即,不论传递过来的究竟是类的哪个对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

C++的多态机制

根据绑定时间和实现方式的不同,C++中的多态可以分为:

形式 决议
函数重载 编译期
操作符重载 编译期
模板 编译期
虚函数 运行时

编译期多态包括了重载和模板,对同一个接口,C++允许定义不同的参数列表来实现不同的行为;而运行时多态性是通过类的继承来实现的,通过重载父类虚函数,父类和子类以同一个接口实现不同的行为。

根据函数地址绑定的时间不同,多态也可以分为静态多态和动态多态。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,那么就是静态多态。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于动态多态。

一些实例

下面给出一些巧妙结合了多态和模板的例子。

  • 一个通用的Callable类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <memory>
#include <iostream>
#include <functional>

// a generic callable class
class FunctionWrapper {
// base class
struct impl_base {
virtual void call() = 0;
virtual ~impl_base() = default;
};

// template child class
template <typename Function>
struct impl_type : impl_base
{
Function f_;
explicit impl_type(Function f) : f_(f) {}
void call() final { f(); }
};

// private callable object
std::unique_ptr<impl_base> impl;
public:
// constructor
template <typename Function>
FunctionWrapper(Function f)
: impl(new impl_type<Function>(std::move(f)) {}

// operator overloading
void operator() () { impl->call(); }

// copy
FunctionWrapper(const FunctionWrapper&) = delete;
FunctionWrapper&operator=(const FunctionWrapper&) = delete;

// move
FunctionWrapper(FunctionWrapper&& other) noexcept
: impl(std::move(other.impl)) {}

FunctionWrapper&operator=(FunctionWrapper&& other) noexcept {
impl = std::move(other.impl);
return *this;
}
};

struct test {
bool operator() () {
std::cout << "this is a callable object." << std::endl;
}
};

void print(int i) {
std::cout << "print: " << i << std::endl;
}

int main(int argc, char* argc[]) {
auto lambda = []() {
std::cout << "this is a lambda." << std::endl;
}
auto func = std::bind(&print, 10);

// wrapper
FunctionWrapper f1(lambda);
FunctionWrapper f2(func);
FunctionWrapper f3(test);

// do something here

// call
f1();
f2();
f3();
}

通过继承+模板的方式,FunctionWrapper擦除了可调用对象的类型,实现了一种统一调用的方式,这样的Callable广泛地用于回调、线程池等场景。

  • 奇异模板递归模式

奇异递归模板模式(curiously recurring template pattern,CRTP)是C++模板编程时的一种惯用法(idiom):把派生类作为基类的模板参数。它也被称作F-bound polymorphism,相关介绍可以参考 wiki

1. CRTP的特点

  • 继承自模板类;
  • 使用派生类作为模板参数特化基类;

2. CRTP基本范式

CRTP如下的代码样式:

1
2
3
4
5
6
7
8
9
10
template <typename T>
class Base
{
private:
};

// use the derived class itself as a template parameter of the base class
class Derived : public Base<Derived>
{
};

这样做的目的是在基类中使用派生类,从基类的角度来看,派生类其实也是基类,通过向下转换downcast,因此,基类可以通过static_cast把其转换到派生类,从而使用派生类的成员,形式如下:

1
2
3
4
5
6
7
8
9
10
template <typename T>
class Base
{
public:
void doWhat()
{
T& derived = static_cast<T&>(*this);
// use derived...
}
};

3. 一个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

template <typename T>
class Base {
friend T;
private:
// prevent this kind of hierarchy
// class Derived2 : public Base<Derived>
Base(){};

public:
void interface()
{
static_cast<T*>(this)->implementation();
}
};

struct Derived : Base<Derived>
{
void implementation()
{
cerr << "Derived implementation\n";
}
};

int main()
{
Derived d;
d.interface(); // Prints "Derived implementation"

return 0;
}

4. 常见使用

  • 数学库Eigen、点云库PCL等三方库中广泛地使用了这一技巧来精简代码;
  • std::enable_shared_from_this,在回调技术中至关重要的一个类,在后面的文章中会重点介绍(现在不想写)。

总结

下面是一些简单的总结

静态多态

优点:

  • 由于静多态是在编译期完成的,因此效率较高,编译器也可以进行优化;
  • 有很强的适配性和松耦合性,比如可以通过偏特化、全特化来处理特殊类型;
  • 模板编程为C++带来了泛型设计的概念,这在STL库中得到淋漓尽致的体现。

缺点:

  • 由于是模板来实现静态多态,因此模板的不足也就是静多态的劣势,比如调试困难、编译耗时、代码膨胀、编译器支持的兼容性;
  • 不能够处理异质对象集合;

动态多态

优点:

  • OO设计,对是客观世界的直觉认识;
  • 实现与接口分离,可复用;
  • 处理同一继承体系下异质对象集合的强大威力

缺点:

  • 运行期绑定,导致一定程度的运行时开销;
  • 编译器无法对虚函数进行优化;
  • 笨重的类继承体系,对接口的修改影响整个类层次;

多态并不是银弹,因地制宜、见招拆招才是解决问题最通用的办法。