unique_ptr和正向声明:编码工厂函数的正确方法

时间:2019-04-20 03:33:09

标签: c++ c++11 smart-pointers forward-declaration

最近学习了智能ptrs,我正在尝试编写一个返回unique_ptrs的工厂函数。阅读了几篇有关将创建时间以及显式定义的ctor和dtor放在同一个cpp文件中的文章之后,我认为我可以做到这一点:

// factory.hpp

struct Foo;

std::unique_ptr<Foo> create();
// foo.cpp

struct Foo {
    Foo();
    ~Foo();
    Foo(const Foo &);
    Foo(Foo &&);
};

std::unique_ptr<Foo> create() {
    return make_unique<Foo>();
}
#include "factory.hpp"


int main() {
    auto r = create();
    return 0;
}

但是我遇到了不完整的类型错误。然后经过几个小时的网络搜索和实验, 我意识到我什至无法做到这一点:

这是经典的unique_ptr Pimpl惯用法。

// A.hpp

struct B;

struct A {
    A();
    ~A();
    unique_ptr<B> b;
};
// A.cpp

struct B {};

A::A() = default;

A::~A() = default;

#include "A.hpp"


int main() {
    A a;   // this is fine since we are doing the Pimpl correctly.

    // Now, I can't do this.
    auto b = std::move(a.b);   // <--- Can't do this.

    return 0;
}

为便于讨论,请忽略std::move行没有限制的事实。 我遇到同样的不完整类型错误。

以上两种情况基本相同。经过一番搜索,我想我理解了错误的原因, 但是我想要一些指针(双关语意)和你们的确认。

  1. 删除不完整的类型是UB。这就是为什么禁止使用默认删除器创建带有不完整类型的unique_ptrs的原因。
  2. 如果我使用自定义删除器,我应该能够做到。
  3. 我正在猜测,因为我使用的是默认删除器,由于某种原因,我不太确定。

明确定义创建和销毁功能应该可以解决问题。但是对我来说,这很丑。首先,在我的情况下,默认删除器将起作用。 另外,在我看来,我不能将lambda用作毁灭者,因为lambda的类型只有编译器才知道, 而且我无法使用decltype进行工厂函数声明。

所以我的问题是:

  1. 此失败的原因是什么?
  2. 编写返回unique_ptrs的工厂函数的正确方法是什么?

如果我所说的话有误,请纠正我。任何指针将不胜感激。

1 个答案:

答案 0 :(得分:2)

当编译器实例化std::unique_ptr<Foo>的析构函数时,编译器必须找到Foo::~Foo()并对其进行调用。这意味着Foostd::unique_ptr<Foo>被销毁时必须是完整类型。

此代码很好:

struct Foo;

std::unique_ptr<Foo> create();

...只要您不需要调用std::unique_ptr<Foo>的析构函数!对于将std::unique_ptr返回到类的工厂函数,该类需要为完整类型。这是声明工厂的方式:

#include "foo.hpp"

std::unique_ptr<Foo> create();

您似乎在正确地使用std::unique_ptr实现pimpl。您必须在A::~A()完成时(在cpp文件中)定义B。您必须在同一位置定义A::A(),因为如果要分配内存并调用其构造函数,B必须完整。

这很好:

// a.hpp

struct A {
  A();
  ~A();

private:
  struct B;
  std::unique_ptr<B> b;
};

// a.cpp

struct A::B {
  // ...  
};

A::A()
  : b{std::make_unique<B>()} {}

A::~A() = default;

现在让我们考虑一下(我们假装我没有将b设为私有):

int main() {
  A a;
  auto b = std::move(a.b);
}

这到底是怎么回事?

  1. 我们正在构建std::unique_ptr<B>来初始化b
  2. b是一个局部变量,这意味着将在范围的末尾调用其析构函数。
  3. 实例化B的析构函数时,
  4. std::unique_ptr<B>必须是完整类型。
  5. B是不完整的类型,因此我们无法销毁b

好的,所以如果std::unique_ptr<B>是不完整的类型,则不能绕过B。此限制是有道理的。 pimpl的意思是“实现的指针”。外部代码无法访问A的实现,因此A::b应该是私有的。如果您必须访问A::b,那么这不是pimpl,这是另外一回事。

如果在隐藏A::b的定义的同时确实必须访问B,则有一些解决方法。

std::shared_ptr<B>。这样会多态删除对象,以便在实例化B的析构函数时,std::shared_ptr<B>不必是完整的类型。它的速度不如std::unique_ptr<B>快,我个人宁愿避免使用std::shared_ptr,除非绝对必要。

std::unique_ptr<B, void(*)(B *)>。与std::shared_ptr<B>删除对象的方式类似。函数指针在负责删除的结构上传递。这具有不必要携带函数指针的开销。

std::unique_ptr<B, DeleteB>。最快的解决方案。但是,如果您拥有少数几个pimpl(但不是真正的pimpl)类,可能会有点烦,因为您无法定义模板。这是您的操作方式:

// a.hpp

struct DeleteB {
  void operator()(B *) const noexcept;
};

// a.cpp

void DeleteB::operator()(B *b) const noexcept {
  delete b;
}

定义自定义删除器可能是最好的选择,但是如果我是你,我会找到一种避免需要从类外部访问实现详细信息的方法。