禁止模板虚拟功能是否是一种不必要的谨慎

Is forbidding template virtual functions an unnecessary cautiousness?

本文关键字:一种 不必要 虚拟 功能 是否是 禁止      更新时间:2023-10-16

看了很多类似话题的帖子,思考了一段时间,还是不明白为什么禁止实现模板虚函数。 在我看来,这种情况与将静态多态性与动态多态性混合无关,而是在编译时使用函数的模板微分,然后在运行时对每个单独创建的函数使用动态多态性。

考虑这段代码:

class parrent{
public:
virtual float function(float value)const{
return value;
}
virtual double function(double value)const{
return value;
}
virtual long double function(long double value)const{
return value;
}
virtual ~parrent() = default;
};
class a_child:public parrent{
public:
float function(float value)const override{
return value + 1.5;
}
double function(double value)const override{
return value + 1.5;
}
long double function(long double value)const override{
return value + 1.5;
}
};

显然,这段代码是可以的,并且会达到预期的结果。 但是使用模板重写类似的代码:

class parrent{
public:
template<typename t__>
virtual t__ function(t__ value)const{
return value;
}
virtual ~parrent() = default;
};
class a_child:public parrent{
public:
template<typename t__>
t__ function(t__ value)const override{
return value + 1.5;
}
};

是不允许的。

我不是编译器设计师,但根据我所读到的内容,编译器将从虚函数创建一个查找表,并使用它们在运行时启动适当的函数,这与它们在模板函数的情况下所做的不同。对于在编译时使用模板函数的任何模板参数集,编译器将创建一个唯一的函数。 对于此示例,编译器只需查看此虚拟模板函数在整个程序中的使用方式,即可在编译时检测模板参数。现在请考虑主要功能:

int main() {
parrent* a;
parrent* b;
a = new parrent;
b = new a_child;
std::cout<< a->function(1.6f) << std::endl;
std::cout<< a->function(1.6) << std::endl;
std::cout<< a->function(1.6L) << std::endl;
std::cout<< b->function(1.6f) << std::endl;
std::cout<< b->function(1.6) << std::endl;
std::cout<< b->function(1.6L) << std::endl;
delete a;
delete b;
return 0;
}

在这里,编译器将看到该函数用于浮点值一次,一次用于双精度值,一次用于长双精度值,因此在任何情况下它都可以轻松地使用适当的模板参数创建正确的函数。 最终将有 3 个单独的虚拟功能,而不仅仅是一个虚拟功能。 如果我们有一个函数,模板参数无法从函数输入中推断出来,例如

template<typename t__>
virtual t__ function(int value){return value;}

然后用户可以自己给出参数,例如:

object_pointer->function<double>(1234);

这些做法正是已经在任何模板函数中使用的,那么为什么虚拟函数会有所不同!

我能想到的这种做法的唯一警告是,当模板虚拟函数从子对象实例化而不是从父对象或指针实例化时。 即使在这种情况下,也可以应用相同的实践来创建不同的虚拟函数。或者,由于缺乏对虚拟性的使用,它们可以成为正常的个人功能。

从答案和评论来看,这种方法可能存在严重的问题,这对其他人来说都是显而易见的,所以请耐心等待并帮助我理解它。

我想答案中提到的问题与编译器和/或链接器无法知道它应该为一个类生成多少(以及什么类型的)vtable有关其余代码或它可能面临的不同翻译单元。

好吧,假设它可以生成一个未完成的 vtables 列表并随着时间的推移对其进行扩展。在动态链接的情况下,使用虚拟(非模板)函数实例化模板类时,最终会得到两个 vtables 或同一类的两个不同实例的问题已经发生了。 所以编译器似乎已经有办法规避这个问题了!

首先,我们不要忘记,对于c,方法或类非静态函数只不过是需要对象作为其参数之一的简单函数,因此我们不要将类视为一些复杂的代码片段。

其次,我们不要被编译器和链接器以及今天不起作用的东西所迷惑。语言应该是标准的,而不是编译器生成可执行文件的方式!我们不要忘记,标准 c++ 17 中仍有许多功能甚至 GCC 还没有涵盖!

请从逻辑方面向我解释,而不是编译器和/或链接器的工作方式是什么问题?

编译器实现多态类的方式如下:编译器查看类定义,确定需要多少个 vtable 条目,并将该 vtable 中的一个条目静态分配给类的每个虚拟方法。无论在哪里调用这些虚拟方法之一,编译器都会生成从类中检索 vptr 的代码,并在静态分配的偏移量处查找条目,以确定需要调用的地址。

我们现在可以看到拥有虚拟模板会导致问题。假设您有一个包含虚拟模板的类。现在,在类定义结束后,编译器不知道要使 vtable 有多大。它必须等到翻译单元结束,才能看到实际调用的模板专用化的完整列表(或指向成员的指针)。如果类只在这个单一的翻译单元中定义,则可以通过将 vtable 偏移量按遇到的递增顺序分配给模板专用化,然后在末尾发出 vtable,从而解决此问题。但是,如果类具有外部链接,则会崩溃,因为在编译不同的翻译单元时,编译器无法避免在将偏移量分配给虚拟方法模板的专用化时发生冲突。相反,vtable 偏移量必须替换为符号,链接器在看到所有翻译单元的引用专业列表并将它们合并到单个列表中后,这些符号将由链接器解析。似乎如果标准C++需要支持虚拟模板,则每个实现都必须要求链接器来实现此功能。我可以猜测,这在短期内是不可行的。

我不是编译器设计师,但我看到您希望做的事情有问题。

当您具有虚拟模板成员函数时,例如

template<typename t__>
virtual t__ function(t__ value)const{
return value;
}

适用的类型没有尽头。编译器如何知道是否在intdouble停止?可以实例化该函数的类型数量不受限制。您是否希望编译器生成考虑函数实例化的所有可能方式的 vtable?那是无限的。这是不可行的。

相关文章: