C++拾趣——使用多态减少泛型带来的代码膨胀

时间:2022-06-17
本文章向大家介绍C++拾趣——使用多态减少泛型带来的代码膨胀,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

        泛型编程是C++语言中一种非常重要的技术,它可以让我们大大减少相似代码编写量。有时候,我和同事提及该技术时,称它是“一种让编译器帮我们写代码的技术”。(转载请指明出于breaksoftware的csdn博客)

        C++是一门静态语言,它最终的编译成果是可以直接运行于冯诺依曼体系的计算机上,而不像其他动态语言,可以运行于虚拟机等容器中。由于对运行效率得追求,C++也是一门类型精确的语言,即object是什么类型,在编译时往往就要确定好,这种方式可以称为数据的静态绑定。

template<class T>
void call_function(T& f) {
	f();
};

class PrintA {
public:
	virtual void operator ()() {
		std::cout << "Print A" << std::endl;
	}
};

class PrintB : public PrintA {
public:
	virtual void operator ()() {
		std::cout << "Print B" << std::endl;
	}
};

int main() {
	PrintA a;
	PrintB b;
	call_function(a);
	call_function(b);
	return 0;
}

        上例中,我们只写了一个call_function模板函数,但是最终编译器会将其翻译成为两个独立的函数,分别是call_function<PrintA>()和call_function<PrintB>()。这也是之前所述“一种让编译器帮我们写代码的技术”的表现。

        我们逆向上述代码来验证下

        上图我们看到call_function<PrintA>()和call_function<PrintB>()方法的地址是不同的。这就意味着,这两个方法拥有各自的代码逻辑。再上升一个层次去看,使用call_function的模板方法的类有多少种,就会产生多少个相应的特化方法。我们只写了一个模板方法,但是编译器最终帮我们生成了多个,这个过程和现象我们称为发生了“代码膨胀”。

        编译器将类型特化,即精确指定了类型,这就使得C++程序在运行时直接跳转到相应函数地址就行,而不需要做类型判别后去路由。这也是C++高效的一个重要原因。

        除了静态绑定,C++还有半动态绑定。这也是C++实现多态的技术基础。我们可以使用该技术,部分的解决泛型技术带来的“代码膨胀”的问题。

        以上例为例,我们只要增加如下方法就行

void call(PrintA& f) {
	f();
}

        然后调用call(a)和call(b)

        可以看到,两次调用的call方法指向了同一个地址。于是不管call方法操作的类型有多少个,它都没有导致代码的膨胀。

        需要指出的是,泛型和多态在上例中,体现了“空间”和“时间”的选择问题。当我们在做优化代码时,往往最终会走到“时间换空间”或者“空间换时间”的选择中。

        上例泛型技术,生成了多份函数。在调用时,方法对应的函数地址是确定的,于是这是种调用是高效的。这是“空间换时间”的案例。

        上例多态技术,只生成了一份代码。在调用时,call方法需要找到object的虚表,然后计算出虚函数的地址,最后才能调用相应的虚函数。这个过程没有直接call一个地址快。这是“时间换空间”的案例。

        目前CPU的发展已经进入瓶颈,磁盘的空间却越来越便宜。很多人可能觉得“空间换时间”是个更好的选择,其实不可以一概而论。因为如果程序的最终编译产物小,其在CPU指令缓存中发生了cache miss也可能变小,最终效率可能还是可观的。