虚函数模板类中的std :: unique_ptr不完整类型错误

时间:2017-09-29 07:54:24

标签: c++ templates

我遇到了一个小问题,我现在还不明白,也无法找到解释。我读到了如何在PIMPL习语中使用std :: unique_ptr并且它有效但不是在一个奇怪的情况下,这当然发生在我身上。

最简单 - 我将展示一个简单的代码示例,它可以重现问题(使用VS2017社区进行编译)。

header.h ##

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

forward.h

#include "header.h"
#include <iostream>

class Forward {
public:
    ~Forward() {
        std::cout << "HAAA" << std::endl;
    }
};

forward.cpp

#include "forward.h"

TestUniquePtr make_ptr() {
    return TestUniquePtr{ new Forward };
}

的main.cpp

由于'无法删除不完整类型'而无法编译的文件。 请注意,此处甚至没有调用函数foo。 那么编译器应该尝试在这个单元中编译这个函数? 如果此函数不是虚函数或TestForward不是模板 - 它可以工作。

#include "header.h"

int main (int argc, char *argv[]) {
    TestForward<3> a;
    return 0;
}

我知道如何解决这个问题 - 通过定义我的不是模板的删除器,并在forward.cpp中编写它的定义但是...我认为这应该有效,所以请帮我找出为什么模板+虚拟化它不工作:(

1 个答案:

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