在虚函数周围使用 #ifdef 预处理器会导致与库链接的程序中出现运行时错误

问题描述

我试图在虚拟函数周围使用 #ifdef 预处理器。代码的简化版本如下所示:

class Base
{
#ifdef ENABLE_FLAG
    virtual void function1();
#endif //ENABLE_FLAG

    virtual void function2();
    virtual void function3();
};

class Child : public Base
{
#ifdef  ENABLE_FLAG
    void function1() override;
#endif //ENABLE_FLAG

    void function2() override;
    void function3() override;
};

代码编译得很好。但是,当我的应用程序调用 Child::function3() 时,由于某种原因,它实际上最终调用了 Child::function2()。我认为预处理器以某种方式弄乱了虚拟表。

我在 Visual Studio 2017 中运行调试模式。我很好奇这个运行时问题的原因是什么。这是依赖于编译器的行为吗?

另一件值得注意的有趣事情是,如果我确保定义了 ENABLE_FLAG 并删除 Child 类中的 #ifdef 子句并将其保留在 Base 类中,那么编译器实际上是在抛出编译错误。它在这里有什么不同?

更新:这个类在主程序和库中都使用。

解决方法

虚拟函数列在每个类的表中 - virtual function tablevtable - 类的每个实例都包含一个指向该表的指针,该表本身包含指向该表的每个虚函数的指针类,按照它们在类声明中列出的顺序 (*)。 (如果您想查看,有大量介绍虚拟表的教程文章和视频。)

因此,如果您在启用该 #ifdef 的情况下编译程序的部分,并且使用该 #ifdef 不编译程序的另一部分 启用 - 不同部分看到的虚拟功能数量不同,vtables 将不同。这不应该发生,并导致您看到的问题。

所以不要那样做。您违反了名为 ODR"One Definition Rule" 的非常严格的 C++ 规则。如果你这样做,一切都悬而未决——也就是说,任何事情都可能出错。

(关于 ODR 的奇怪之处:对于如此严格的关键规则,编译器(即整个编译系统)不需要以任何方式告诉您您违反了它。所以,真的,不要

顺便说一句,所有库和主程序必须看到相同的类声明!上面关于 ODR 的那些东西? C++ 标准不知道“库”——静态、动态或其他。对于它,只有程序和编译单元。 (这些“库”的东西只是编译系统为我们提供的一种便利。)所以所有规则都适用于整个程序——而 ODR 是最常在这里咬你的! (正如你刚刚发现的......)

(*) 一阶近似,不包括多重继承...