一、多态
学习C++编程的开发者可能在一开始就明白了面向对象编程的三大特征即"封装、继承和多态"。多态的实现机制有很多种,这个在前面分析过。一般开发者认为的动态多态就是通过虚拟函数来实现的。通过指针或引用,动态的调用具体的类型的虚拟函数来实现相关的功能。对于代码的灵活性和扩展性有不小的提高。
二、虚函数的机制简述
作为运行时多态进行动态绑定的核心机制,虚函数的核心就在虚表机制。其实,很多面试也多是围绕着虚函数表进行各种的问来问去。
所谓虚表,就是每个类中如果存在着虚函数,一般会有一个隐藏的虚函数表,它主要用来存储所有虚函数的地址。这个表通常会定义在类对象的开始,它会隐式的包含一个虚表的指针vptr,通过其可以来访问类中的虚函数表。当一个继承类中重写了虚函数,那么表对应的位置将会进行相关的修改;同时如果增加了相关的虚函数,则会在表中进行增加(注意,纯虚函数会有标记无法被直接调用)。
而在实际运行过程中,要想真正找到动态绑定执行的函数,就必须在虚表内进行查询,找到相关的函数地址再进行运行。
三、问题和优化方法
通过上面的简单分析是不是发现,虚函数需要一个内存地址存储和动态查找的执行过程。而这两个过程也是拖慢虚函数运行效率的重要原因。有问题就会有解决的方法,虽然使用了动态绑定技术,对性能和执行效率的影响可能会有很大影响,但正是如此,也才有优化的空间。
一般对于动态绑定优化的方法主要有:
- 减少动态绑定的使用
就是尽量在实际开发,把不必要的动态绑定函数去除。减少应用机率 - 使用内联
某些情况下,虚函数也是可以内联的 - 引用高版本的final和override关键字
通过关键字final和override来显式的为编译器优化提供支持 - 编译期去除多态
现代的编译器往往具有在满足条件的情况下去除多态的能力。编译时可以使用编译器优化选项进行处理 - 使用静态多态
这种在前面分析过,可以使用CRTP或宏、模板等技术进行静态多态的处理。 - 引入局部性
这种也好理解,让虚函数表成为热点,在Cache中使用,从而提高调用的效率
当然,最简单的方法还是不使用多态,完全解决问题。但实际的情况可能千差万别,不可能开发者想怎么做就怎么做。
四、例程
下面看一个例子:
c
#include <iostream>
#include <memory>
#include <vector>
constexpr double PI = 3.14159;
// 普通虚函数
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle : Shape {
double r;
explicit Circle(double r) : r(r) {}
double area() const override { return PI * r * r; }
};
struct Rect : Shape {
double w, h;
Rect(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
};
double sumV(const std::vector<std::unique_ptr<Shape>> &shapes) {
double sum = 0;
for (const auto &s : shapes) {
sum += s->area(); // 虚调用
}
return sum;
}
// final去虚化
struct FinalCircle final : Shape {
double r;
explicit FinalCircle(double r) : r(r) {}
double area() const override { return PI * r * r; }
};
double finalCall(const FinalCircle &c) { return c.area(); }
// CRTP:静态多态
template <typename Derived> struct StaticShape {
double area() const { return static_cast<const Derived *>(this)->areaCacl(); }
};
struct StaticCircle : StaticShape<StaticCircle> {
double r;
explicit StaticCircle(double r) : r(r) {}
double areaCacl() const { return PI * r * r; }
};
struct StaticRect : StaticShape<StaticRect> {
double w, h;
StaticRect(double w, double h) : w(w), h(h) {}
double areaCacl() const { return w * h; }
};
template <typename ShapeType> double sumS(const std::vector<ShapeType> &shapes) {
double sum = 0;
for (const auto &s : shapes) {
sum += s.area();
}
return sum;
}
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.emplace_back(std::make_unique<Circle>(1.0));
shapes.emplace_back(std::make_unique<Rect>(3.2, 2.0));
std::cout << "virtual: " << sumV(shapes) << std::endl;
FinalCircle fc(2.0);
std::cout << "final: " << finalCall(fc) << std::endl;
std::vector<StaticCircle> staticCircles;
staticCircles.emplace_back(2.2);
staticCircles.emplace_back(3.3);
std::cout << "CRTP: " << sumS(staticCircles) << std::endl;
}
编译时可以采用下面的优化选项(需要根据情况处理):
g++ -O2 -std=c++17 main.cpp
或:
g++ -O3 -flto -std=c++17 main.cpp
五、总结
在"虚函数的性能"和"内联"中,对虚函数具体的情况进行了分析。特别是哪些情况会真正的拉开虚拟函数执行的效率。虚函数不是效率执行的必然的"杀手",但当它成为"杀手"时,应该能够从上面的优化中找出适应自己开发场景的方式来提高执行速度。