我遇到了一个小问题,我现在还不明白,也无法找到解释。我读到了如何在PIMPL习语中使用std :: unique_ptr并且它有效但不是在一个奇怪的情况下,这当然发生在我身上。
最简单 - 我将展示一个简单的代码示例,它可以重现问题(使用VS2017社区进行编译)。
Forward类的前向声明,以及具有返回unique_ptr的虚函数的模板类TestForward。
class Forward;
using TestUniquePtr = std::unique_ptr<Forward>;
TestUniquePtr make_ptr();
template<int a>
class TestForward {
public:
virtual TestUniquePtr foo();
};
template<int a>
TestUniquePtr TestForward<a>::foo() {
return make_ptr();
}
#include "header.h"
#include <iostream>
class Forward {
public:
~Forward() {
std::cout << "HAAA" << std::endl;
}
};
#include "forward.h"
TestUniquePtr make_ptr() {
return TestUniquePtr{ new Forward };
}
由于'无法删除不完整类型'而无法编译的文件。
请注意,此处甚至没有调用函数foo
。
那么编译器应该尝试在这个单元中编译这个函数?
如果此函数不是虚函数或TestForward不是模板 - 它可以工作。
#include "header.h"
int main (int argc, char *argv[]) {
TestForward<3> a;
return 0;
}
我知道如何解决这个问题 - 通过定义我的不是模板的删除器,并在forward.cpp中编写它的定义但是...我认为这应该有效,所以请帮我找出为什么模板+虚拟化它不工作:(
答案 0 :(得分:2)
这里有很多事情,所有人都在一起玩这个错误...
首先,请考虑一下:C ++标准说如果你这样做:
struct Incomplete;
void foo(Incomplete* p) { delete p; }
这是合法的,但是如果Incomplete
的完整定义变成了一个非平凡的析构函数,那么程序就会有不确定的行为。我认为这仅仅是为了兼容早期的C类C ++程序。
因此,为了提高程序的安全性,unique_ptr
的默认删除器使用“安全删除”,即无法为不完整类型编译的删除。这意味着unique_ptr
析构函数的实例化必须知道指向类的完整定义。
在您的程序中,任何使用TestUniquePtr
析构函数的代码都必须知道Forward
的完整定义。
TestForward::foo
使用析构函数。 make_ptr
返回一个对象。 foo
move-从此对象构造自己的返回值,然后销毁源。 (在实际生成的代码中,这很可能是通过返回值优化来优化的,但是如果没有它,代码仍然必须有效。)
使用TestForward<3>::foo
的地点/原因是什么?好吧,因为它是虚拟的,所以必须在实例化类的vtable时实例化它。并且因为它是模板实例化,所以无论在何处调用构造函数,都会实例化vtable(因为构造函数需要将vtable指针写入对象)。并在main
中调用构造函数。
如果foo
不是虚拟的,则无需实例化它。如果TestForward
不是模板,我猜你会将foo
放入一些单独的源文件而不是标题中,因此main
没有显示错误。
那你怎么解决这个问题?
在典型的Pimpl上下文中,您可以通过严格控制实例化unique_ptr
的析构函数来解决此问题。您显式声明了接口类的析构函数,并将该定义放入已知impl类定义的源文件中。
但是,如果您想将unique_ptr
作为不透明句柄分发给不完整的类,则需要替换默认删除器。
// header.h
class Forward;
struct ForwardDeleter {
void operator ()(Forward* ptr);
};
using TestUniquePtr = std::unique_ptr<Forward, ForwardDeleter>;
// forward.cpp
class Forward { ... };
void ForwardDeleter::operator ()(Forward* ptr) { delete ptr; }