From Addison Wesley: c++ Templates
成员函数模板不能宣布虚拟。这个约束条件是因为通常实现虚函数调用机制使用固定大小的表每个虚函数有一个条目。然而,实例化的数量的成员函数模板不是固定,直到整个程序有被翻译。
上面的引用是否意味着模板有静态绑定而虚函数有动态绑定,这就是不能有虚函数模板的原因?请看看能否用外行的语言解释一下。
是,也不是。
解析虚函数调用最流行的方法是使用一个表("vtable"),其中每个虚函数映射到表中的一个索引。这或多或少要求您知道表的大小。
使用模板,将根据需要在不同模块中创建新函数。然后,您要么必须说服链接器在计算出函数的最终数量后构建表,要么使用某种运行时结构在运行时搜索可用的函数。
在许多系统上,链接器是操作系统的一部分,对c++一无所知,所以选择是有限的。运行时搜索当然会对性能产生负面影响,可能对所有虚函数都是如此。
所以,最后,我们决定不值得在语言中引入虚拟模板。
考虑:
struct X
{
template <typename T>
T incr(const T& t)
{
return t + 1;
}
};
由于incr()
应用于不同的T类型,生成了新的函数。在app.c++
中有:
X x;
x.incr(7); // incr<int>()
x.incr(7.0); // incr<double>()
x.incr("hello"); // incr<const char*>()
然后当它编译app.c++
时,它看到3个函数-如果incr
被允许为virtual
-它可以在虚拟调度表中为上面的三个实例化x腾出空间。然后假设它在运行时加载一个共享库,并且该库的代码有X::incr
的两个实例化uint32_t
和std::string::const_iterator
。dlopen()
需要为已经创建的对象增加现有的虚拟调度表,以便为两个新函数腾出空间。听起来不太可怕,但是考虑一下:
调用虚函数的每个代码位必须知道这些函数的地址在运行时是否被偏移了一些(由于动态加载额外的实例),因此在每个虚调度中都有额外的内存和性能成本
当存在多重继承,或者派生类本身是派生的时候,编译器可能想要为整个虚函数集创建一个虚分派表(一种选择,有许多实现虚分派的方法):在这种情况下,新的虚函数要么取代其他类的虚函数,要么需要与现有的虚函数分离。同样,在任何方案中都需要更多的运行时开销来管理它。
因此,在非常罕见的情况下,这可能是有用的,不值得牺牲和复杂化更常见的非模板虚拟的情况。
上面的引用是否意味着模板有静态绑定而虚函数有动态绑定,这就是不能有虚函数模板的原因?
基本上,是的。更具体地说,当生成代码以支持动态绑定时,静态绑定会导致问题。
当编译器编译基类时,它找到一个虚函数并决定创建一个虚函数表——这将用于实现动态绑定:当在派生实例上调用虚函数时,编译后的代码遵循实例中指向派生类虚函数表的指针,然后是该表中指向虚函数实现的指针。这个表必须包含所有可能被调用的虚函数。现在,假设我们创建了一个模板化的虚函数。对于模板的每个实例化,函数表都需要一个条目,因为这些函数中的任何一个都可能在运行时被调用。但是,关于模板实例化的类型的信息(通常)不能在生成虚函数表时收集。
虚函数和模板仍然可以很好地一起工作,只是有一个小的特殊情况没有实现。
template<class T>
class A { virtual void f()=0; }; // works fine
class A { template<class T> virtual void f(T t)=0; }; // does not work
取决于绑定的含义。
可以通过调用成员模板实现虚方法。只要内联它,任何带有尾部调用优化的编译器都会消除开销
Sorta.
您不能真正"覆盖"未实例化的template
,因为它甚至不存在于编译的应用程序中。如果您实例化它,那么您就不会重写模板,而只是另一个普通的函数。: -)