【问题标题】:Unexpected behaviour with a pure virtual function overridden in a derived type [duplicate]在派生类型中重写纯虚函数的意外行为[重复]
【发布时间】:2014-06-19 22:52:43
【问题描述】:

这是我认为我理解 C++ 虚方法相当好的情况之一,然后出现了一个例子,我意识到,遗憾的是,我没有。有没有读过这篇文章的人可以理解以下内容?

这是一些测试代码,我在其中定义了一个非常简单的基类(实际上只是一个双元素结构),一个包含虚拟 void 方法的抽象模板类,然后是一个从它们两者显式继承的派生类用具体方法覆盖虚拟 void 方法。

#include <string.h>  // For memcpy
#include <vector>    // For std::vector

struct int_array_C
{
    int  n;
    int* contents;
};

template <typename T> class array_template
{
public:
    array_template<T>() {}

    array_template<T>(const array_template<T> &source) 
    {
        *p_n = *(source.p_n);
        setPointers(&(source.local_contents[0]));
    }

    // ..and in reality, a bunch of other array manipulation functions

protected:
    virtual void setPointers(const T* data) = 0;

    int *p_n;
    std::vector<T> local_contents;
};

class int_array : public int_array_C, public array_template<int> 
{
public:
    int_array() : array_template<int>() 
    { 
        n = 0; contents = NULL; 
    }

protected:
    virtual void setPointers(const int* data)
    {
        p_n = &n;
        local_contents.resize(n);
        memcpy(static_cast<void *>(&local_contents[0]), 
               static_cast<const void *>(data), n*sizeof(int));
        contents = &local_contents[0];
    }
};

int main()
{
    int_array myArray;
    int_array yourArray(myArray);
    return 1;
}

在 main() 的第二行调用复制构造函数时,参数是派生类的实例,该类具有具体的 setPointers() 方法。因此,当调用模板类的复制构造函数并遇到对 setPointers() 的调用时,我希望多态性规则开始生效并调用 派生 类的 setPointers() 方法。

事实上,编译器对此感到窒息;在编译时我收到警告说

"Warning: call of pure virtual function at line 18" 

在链接时链接器失败并显示一条消息

error LNK2019: unresolved external symbol "protected: virtual void __cdecl array_template<int>::setPointers(int const *)" (?setPointers@?$array_template@H@@MEAAXPEBH@Z) referenced in function "public: __cdecl array_template<int>::array_template<int>(class array_template<int> const &)" (??0?$array_template@H@@QEAA@AEBV0@@Z)

在 Windows 上使用 Visual C++ 和 Intel C++ 以及在 Linux 上使用 gcc 会发生完全相同的事情(错误消息的文本略有不同),因此这显然是对语言规则的真正违反,而不仅仅是编译器的怪癖。但我看不出问题出在哪里。

那么,我做错了什么,我怎样才能使这项工作按预期进行?

【问题讨论】:

  • 代码有很多错误,伙计。
  • 你不能从基类构造函数中调用派生类的虚函数,因为在调用它的时候派生类还没有被构造出来。
  • 调用是错误的,但我仍然不明白为什么这最终是链接错误而不是运行时 UB
  • 我不同意这是引用答案的重复的断言。确实有很多事情会在链接时导致未定义的引用错误,但我在列出的那些中找不到这个。正如下面所讨论的(并由@Paul Griffiths 指出),这个问题的关键是基类方法和派生类方法的实例化顺序,我在引用的答案中找不到任何相关信息。 Cheers 和 hth 引用的常见问题解答。 Alf 的相关性更高,但如果没有 Paul Griffiths 的评论,我想我什至不会在那里找到答案。

标签: c++ templates inheritance virtual overriding


【解决方案1】:

在执行类T 的构造函数主体时,动态类型为T。因此,此处对纯虚拟 T 成员函数的任何直接或间接调用都是无效的。这是 C++ 的一些额外类型检查安全性,确保在派生类子对象正确初始化(或至少有机会)之前不会意外进入派生类代码。

但是,我不明白为什么会导致链接错误。

关于解决方案,这里是a FAQ;本质上,您必须以某种方式将派生类功能或数据传递给基类,并且有很多方法可以做到这一点。碰巧我曾经说服马歇尔包括那个常见问题解答项目。所以我可以在某种程度上声称它是我对你问题的回答。 :-)


更新原始代码行为。

添加以下内容:

template< class T >
void array_template<T>::setPointers( const T* data )
{
    using namespace std;
    clog << "!pure virtual setPointers called!" << endl;
}

事实证明,Visual C++ 12.0 和 g++ 4.8.2 都调用了它。

调用是未定义的行为,所以两个编译器都有权产生这种行为,但它仍然让我感到惊讶。我认为他们会在 vtable 中放置一个指向某些错误函数的指针。无论如何,它解释了这两个编译器的链接错误。

【讨论】:

  • 链接器错误在this SO answer 中有更多解释。纯虚方法可以按照here 的解释定义,但我也不希望在这种情况下调用它。
  • @uesp:为什么两个编译器都会在调用的类中发出对实现的调用,这是令人困惑的问题,David 没有回答。调用是 UB 和微不足道的,所以他们可以做到这一点。这是不切实际的,他们都这样做,这让我感到困惑;没看懂,但是觉得有什么要理解的。
  • 如果函数是在另一个翻译单元中定义的,为什么就不行了?
  • @PaulGriffiths:即使该类是一个模板,它的相关专业化也可以在另一个翻译单元中定义,那里没有问题。问题很简单,一个T类纯虚函数的虚调用,当动态类型为T时,不管函数是否定义,都是UB。处理这个问题的一个实用方法是在 T vtable 中放置一个产生错误的存根。链接错误很好,但如图所示,它不能被依赖,所以它不太实用,只是令人困惑。
【解决方案2】:

Paul Griffiths 在我原来的问题下发表了评论:

“你不能从基类构造函数中调用派生类的虚函数,因为在调用它的时候派生类还没有被构造出来。”

这是关键:调用发生在构造函数中,并且因为基类的构造函数在派生类存在之前被调用,所以 还没有具体版本的 setPointers 可以调用(至少,不在正在创建的新对象中;作为参数传递的旧对象有一个这一事实与此无关)。

解决方案非常简单:将复制构造函数完全移出模板并将其放置在派生类中。它只是一个两行函数,因此对我的十几个派生类中的每一个都执行此操作并不会太痛苦,并且它解决了问题。

[注:如果有人想知道这一切有什么帮助,这是一个大型混合语言应用程序的一部分。我想拥有一组容器函数,它们在 C++ 中使用时具有 std::vector 的功能,这样我就可以随意调整大小、引用、复制等,然后传递给一个只需要基础的普通 C 子例程结构。除了模板复制构造函数之外的所有东西都已经运行了很长时间了!]

【讨论】:

  • 请注意,默认构造函数(在当前代码中)为成员 p_n 留下了一个不确定的指针值。
  • 谢谢,发现了。
【解决方案3】:

您不能从基类构造函数调用virtual 方法并获取派生类方法:这些方法是在构造基类之后设置的。

尝试更改继承的布局:

class int_array_impl:public int_array_C{...};
template<class T,class B>class array_template:public B{
  //...
};
typedef array_template<int, int_array_impl> int_array;

使该方法成为非虚拟方法,去掉 array_template 中的纯虚拟内容,然后直接调用 this-&gt;method。如果B 有,就会被调用。

前向构造函数,或者在array_template 下有一个提供自定义构造函数的第四类。

【讨论】:

  • 谢谢,如果 Paul Griffiths 没有先发表评论,我会接受这个答案。