一个模板化的指针类是否可以有虚析构函数?

5

当我使用自制指针类实现pimpl习惯用法时,我遇到了一个令人惊讶的发现(我知道:为什么要自己造轮子呢?但请耐心听我说)。以下三个文件包含了一个最小化的示例:

Pointer.h:

#pragma once 

template <typename T>
class Pointer
{
public:
    Pointer(T*p=0)
        : _p(p)
    {
    }
    virtual ~Pointer()
    {
        delete _p;
    }
private:
    void operator=(const Pointer&);
    Pointer(const Pointer&);

private:
    T*_p;
};

Foo.h:

#pragma once
#include "Pointer.h"

struct Foo
{
    Foo();
    ~Foo();

private:
    void operator=(const Foo&);
    Foo(const Foo&);

private:
    Pointer<struct FooPrivate> p;
};

main.cpp:

#include "Foo.h"

int main(int argc, char* argv[])
{
    Foo foo;
    return 0;
}

不用关心Foo.cpp的内部结构。当我使用MSVC 2008编译main.cpp时,会出现警告:

pointer.h(13) : warning C4150: deletion of pointer to incomplete type 'FooPrivate'; no destructor called

可以通过在指针析构函数中删除关键字virtual来避免此警告。

这对我来说没有任何意义。这个警告是合法的吗?还是MSVC编译器的一个bug?如果是后者,我可以安全地忽略这个警告吗?

我知道在这种情况下使析构函数虚拟化是毫无意义的,但请记住,这只是一个最小可编译示例。我的原始代码要复杂得多。


1
当你写下“请耐心等待”时,我以为你会在后面的帖子中介绍自己编写智能指针的原因。可惜并没有。所以,我不得不问一下——为什么? - Björn Pollex
你有没有研究过std::auto_ptr (http://www.cplusplus.com/reference/std/memory/auto_ptr/) 或者Boost智能指针 (http://www.boost.org/doc/libs/1_46_1/libs/smart_ptr/smart_ptr.htm)? - yasouser
“Foo.cpp” 的内部对于这里非常重要……你是否有一个显式的 ~Foo(),还是使用编译器提供的默认析构函数?你无需采取任何措施,一个空的函数体就可以了,只要在编写它之前 FooPrivate 在 “Foo.cpp” 中已经完全定义好了。 - Dennis Zickefoose
1
@Dennis:~Foo()在类定义中声明,因此它是用户定义的(或根本未定义,在这种情况下,稍后应该会出现链接错误)。Foo.cpp的内容与main.cpp的编译无关,因为它们是不同的翻译单元,而这是一个编译时而不是链接时的警告。 - Steve Jessop
@Steve:我其实不是想发布那条评论,而是选择创建一个正确的答案。然而,理论上,在FooPrivate定义之前就可能发生用户自定义析构函数...但由于没有virtual警告消失了,几乎可以确定这不是问题的原因。 - Dennis Zickefoose
4个回答

4
没有使用virtual,析构函数只会在一个地方被调用;在~Foo内部,在此时你可能已经完全定义了FooPrivate。如果在其他地方创建了另一个Pointer<FooPrivate>实例,则可能会收到警告,但由于你没有,编译器可以告诉你正在安全地行为。
使用virtual,你可以从理论上派生出Pointer<FooPrivate>,并且该新对象可能会从FooPrivate未完全定义的某个地方被销毁。编译器不能确定您是否这样做,因此会发出警告。在这种微不足道的情况下,您可以安全地忽略它,但是如果您确实需要虚析构函数,最好将其牢记在心。

第二段是什么意思?请提供一个具体的例子。 - Cheers and hth. - Alf
@Alf:老实说,我很难让VS2010在任何情况下发出有关此警告的信号,而且我无法访问VS2008。重要的是,验证析构函数可能被多态调用的位置比验证析构函数可能被静态调用的位置更困难。对于非虚拟析构函数,所有销毁都必须静态完成。一旦引入了虚拟析构函数,编译器显然会选择谨慎处理。在所有情况下,这可能是一个错误的阳性,我不确定。 - Dennis Zickefoose
我没有问题让VS2010发出警告(我没有使用任何特殊选项),但我仍然不理解你的第二段话。我怀疑它对这段代码没有意义。一个具体的例子会很好。干杯! - Cheers and hth. - Alf

2

既然你为类Foo提供了析构函数,那么这个警告显然是错误和无用的。

我添加了以下代码来检查,在文件[foo.cpp]中:

#include "foo.h"
#include <iostream>
using namespace std;

struct FooPrivate
{
    FooPrivate() { cout << "FooPrivate::<init>" << endl; }
    ~FooPrivate() { cout << "FooPrivate::<destroy>" << endl; }
};

Foo::Foo()
    : p( new FooPrivate )
{
    cout << "Foo::<init>" << endl;
}

Foo::~Foo()
{
    cout << "Foo::<destroy>" << endl;
}

您得到的警告与Visual C++ 10.0相同,但输出如下:

FooPrivate::<init>
Foo::<init>
Foo::<destroy>
FooPrivate::<destroy>

显然,可执行文件并没有按照愚蠢的警告所说的那样运行...

祝好!


我现在意识到我应该发布Foo.cpp的详细信息。它看起来和你的一模一样。请注意,它可以编译而没有警告。是main.cpp发出了一个警告。 - bgp2000

0

在不完整的类型上调用delete是未定义行为。


你怎么知道在 Foo::~Foo 的位置上它是不完整的? - Luc Danton
只有当不完整类型的析构函数在完成后被证明是非平凡的时候,才会执行。 - Steve Jessop

0
由于您没有完整定义 FooPrivate,编译器不知道它的虚函数表长什么样子。因为它无法调用找不到的虚函数,所以它会退出。

实际上它并没有退出。它只是发出了一个无关紧要的愚蠢警告,然后继续发出正确的代码... - Cheers and hth. - Alf

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接