【问题标题】:std::unique_ptr incomplete type error in virtual function template class虚函数模板类中的 std::unique_ptr 不完整类型错误
【发布时间】:2018-03-11 02:08:28
【问题描述】:

我遇到了一个小问题,我现在不明白,也找不到解释。我读到了如何在 PIMPL 成语中使用 std::unique_ptr 并且它可以工作,但是......不是在一种奇怪的情况下,这当然发生在我现在。

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

header.h ##

Forward类的Forward声明,模板类TestForward有返回unique_ptr的虚函数。

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 但是.. 我认为这应该可行,所以请帮助我找出为什么模板+虚拟使它成为不工作:(

【问题讨论】:

  • foo 无论如何都会被实例化以填充 vtable。并且类型Forward 必须在foo 定义处完整,因为它涉及到make_ptr 返回的临时对象的移动和销毁。

标签: c++ templates


【解决方案1】:

这里发生了很多事情,以至于所有的人都在这个错误中发挥作用......

首先,考虑一下:C++ 标准规定,如果你这样做:

struct Incomplete;
void foo(Incomplete* p) { delete p; }

这是合法的,但如果Incomplete 的完整定义结果证明具有非平凡的析构函数,则程序具有未定义的行为。我相信这仅仅是为了与早期的类 C 的 C++ 程序兼容。

因此,为了提高程序的安全性,unique_ptr 的默认删除器使用“安全删除”,即对于不完整类型无法编译的删除器。这意味着unique_ptr 析构函数的实例化必须知道指向类的完整定义。

因此,在您的程序中,任何使用TestUniquePtr 析构函数的代码都必须了解Forward 的完整定义。

TestForward::foo 使用析构函数。 make_ptr 返回一个对象。 foo 移动从这个对象构造它自己的返回值,然后销毁源。 (在实际生成的代码中,这很可能被返回值优化优化掉了,但没有它,代码必须仍然有效。)

在哪里/为什么使用TestForward&lt;3&gt;::foo?好吧,因为它是虚拟的,所以无论在哪里实例化类的 vtable,它都必须被实例化。并且由于是模板实例化,所以在调用构造函数的任何地方都会实例化vtable(因为构造函数需要将vtable指针写入对象)。并且构造函数在main中被调用。

如果foo 不是虚拟的,则无需实例化它。如果TestForward 不是模板,我猜你将foo 放入某个单独的源文件而不是标题中,因此main 不会出现错误。


那么你如何解决这个问题?

在典型的 Pimpl 上下文中,您可以通过严格控制谁实例化 unique_ptr 的析构函数来解决此问题。您显式声明接口类的析构函数并将定义放入已知 impl 类定义的源文件中。

但是,如果您想将 unique_ptrs 作为不透明句柄分发给不完整的类,则需要替换默认删除器。

// 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; }

【讨论】:

    猜你喜欢
    • 2012-05-12
    • 2021-08-11
    • 1970-01-01
    • 2015-05-01
    • 1970-01-01
    • 2016-05-05
    • 2014-11-22
    • 1970-01-01
    • 2018-06-04
    相关资源
    最近更新 更多