【问题标题】:Is forbidding template virtual functions an unnecessary cautiousness?禁止模板虚函数是不必要的谨慎吗?
【发布时间】:2019-09-19 14:37:33
【问题描述】:

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

考虑这段代码:

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;
    }
};

不允许。

我不是编译器设计者,但根据我的阅读,编译器将从虚函数创建一个查找表,并使用它们在运行时启动适当的函数,这与他们在模板函数的情况下所做的不同.对于在编译时为使用模板函数而给出的任何模板参数集,编译器将创建一个唯一的函数。 对于这个例子,编译器可以在编译时检测模板参数,只需查看这个虚拟模板函数是如何在整个程序中使用的。现在请考虑 main 函数:

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);

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

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

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

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

好吧,可以说它可以生成一个未完成的 vtables 列表并随着它的进行扩展它。在实例化具有虚拟(非模板)函数的模板类时,可能已经发生在动态链接的情况下以两个 vtable 或同一类的两个不同实例结束的问题。 所以看来编译器已经有了规避这个问题的方法!

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

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

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

【问题讨论】:

  • 您是在学习/研究编译器/语言,还是您真正想要实现的目标?
  • 考虑有多少种方式会破坏动态(运行时)链接。一个 DLL 中使用的 parrent 可能无法在另一个 DLL 中使用,因为它们具有不同的 vtable。
  • 请根据逻辑而不是编译器和/或链接器的工作方式向我解释” - C++ 标准的一个重要方面是可实现性。所以逻辑是标准拒绝了难以/不可能实现的功能。
  • rustyx,如果你所说的对于困难的情况(不是不可能的情况)确实是真的,那么我想是时候从 C++ 继续前进了!

标签: c++ function templates virtual


【解决方案1】:

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

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

【讨论】:

  • 非常感谢,我想现在我可以看到问题出在哪里了。但是正如您所提到的,这在逻辑上并非不可能,只是编译器应该更加努力!而对于不同翻译单元的vtables的拆分和合并问题,如果简单地使用带有虚函数的模板类(这是允许的)而不是带有模板虚函数的类,是不是同样的情况?
  • @AKL 类模板与虚函数有很大不同;在类模板被实例化的每一点,vtable 的大小都是已知的;不同的翻译单元可能包含同一类模板的不同特化的 vtable,但这不会造成任何问题,因为同一类模板的不同特化的 vtable 之间没有关系。
  • @AKL 这意味着 链接器 必须更加努力地工作,这将打破使用 C 链接器链接 C++ 程序的悠久历史。要么这样,要么编译器必须使用其他机制来执行虚拟调度,如果需要与当前机制一样高效,目前还不清楚这种机制是什么。
  • Brian 如果一个类模板在不同的翻译单元中使用相同的模板参数实例化怎么办?那不会为同一个类创建不同的vtables吗?如果是这样,链接器将面临同样的问题!因为它必须再次合并这 2 个 vtable。
  • @AKL 不,相同的 vtable 将在每个翻译单元中发出
【解决方案2】:

我不是编译器设计者,但我发现您希望做的事情有问题。

当你有一个虚拟模板成员函数时,比如

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

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

【讨论】:

  • 感谢您的意见。但我认为编译器没有必要生成一个无穷无尽的 vtable。正如我在我的问题中所描述的,第一个编译器必须为那些已在程序中使用并在编译时可见的类型创建单独的虚函数。然后编译器可以为每个单独的虚函数创建一个有限的 vtable。
  • @AKL 以及编译器如何知道整个程序中使用的所有可能类型?请记住:编译器一次只能看到一个 .cpp 文件……生成调用虚函数的代码需要知道 vtable 的样子……但你只能知道正在运行的完整函数集编译完整个程序后在 vtable 中……看到问题了吗?
  • 谢谢迈克尔。我想我不完全理解你的评论。但是由于每个模板类或模板函数的习惯,编译器应该能够在编译时知道这些模板在整个程序中的位置和使用方式。我错过了什么吗?
  • @AKL 您错过了这样一个事实,即编译器在完成整个程序的编译之前无法知道每个模板在整个程序中的使用位置和方式……加上编译器通常, 永远看不到整个程序,因为它只编译单个源文件……所以编译器必须 a) 总是编译整个程序(它不编译)和 b) 总是编译整个程序两次,一次编译有关程序中发生的事情的信息以及实际生成代码的另一个时间......
  • 顺便说一句,我认为这根本不是 vtable 问题。这就是如何从它们的模板中生成单个函数。接下来是 vtableS 的问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-26
  • 2015-04-30
  • 1970-01-01
  • 2013-08-04
  • 2010-09-29
  • 2020-07-22
  • 1970-01-01
相关资源
最近更新 更多