【问题标题】:unique_ptr and forward declaration: the proper way to code a factory functionunique_ptr 和前向声明:编写工厂函数的正确方法
【发布时间】:2019-04-20 03:33:08
【问题描述】:

最近学习了智能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 的工厂函数的正确方法是什么?

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

【问题讨论】:

  • 您在 factory.hpp 中声明了 create,然后在 foo.hpp 中定义了 create(作为非内联函数)。那里发生了什么?
  • @Kerndog73 抱歉,打错了。已编辑。应该是一个cpp文件。

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


【解决方案1】:

当编译器实例化std::unique_ptr&lt;Foo&gt;的析构函数时,编译器必须找到Foo::~Foo()并调用它。这意味着Foostd::unique_ptr&lt;Foo&gt; 被销毁时必须是一个完整的类型。

这段代码很好:

struct Foo;

std::unique_ptr<Foo> create();

...只要你不需要调用std::unique_ptr&lt;Foo&gt;的析构函数!对于将std::unique_ptr 返回给类的工厂函数,该类需要是完整类型。这就是你声明工厂的方式:

#include "foo.hpp"

std::unique_ptr<Foo> create();

您似乎正确地使用std::unique_ptr 实现 pimpl。您必须在 B 完成的位置(在 cpp 文件中)定义 A::~A()。您必须在同一位置定义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&lt;B&gt; 来初始化b
  2. b 是一个局部变量,这意味着它的析构函数将在作用域结束时被调用。
  3. std::unique_ptr&lt;B&gt; 的析构函数被实例化时,B 必须是完整类型。
  4. B 是不完整的类型,因此我们无法销毁 b

好的,所以如果 B 是不完整的类型,则不能传递 std::unique_ptr&lt;B&gt;。这种限制是有道理的。 pimpl 的意思是“指向实现的指针”。外部代码访问A 的实现是没有意义的,所以A::b 应该是私有的。如果你必须访问A::b,那么这不是 pimpl,这是别的东西。

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

std::shared_ptr&lt;B&gt;。这会以多态方式删除对象,因此当 std::shared_ptr&lt;B&gt; 的析构函数被实例化时,B 不需要是完整类型。它不如std::unique_ptr&lt;B&gt; 快,除非绝对必要,否则我个人更喜欢避免使用std::shared_ptr

std::unique_ptr&lt;B, void(*)(B *)&gt;。类似于std::shared_ptr&lt;B&gt; 删除对象的方式。函数指针在负责删除的构造上传递。这会带来不必要的函数指针开销。

std::unique_ptr&lt;B, DeleteB&gt;。最快的解决方案。但是,如果您有多个 pimpl(但不是真正的 pimpl)类,可能会有点烦人,因为您无法定义模板。你会这样做:

// a.hpp

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

// a.cpp

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

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

【讨论】:

  • 感谢您的回答。我同意您关于避免 shared_ptr 和不必要的函数指针的意见。所以总结一下,如果我不想包含类原型,我必须提供一个自定义删除器。最好的方法是使用像 DeleteB 这样的函子。还有一个后续问题,函子案例不会产生任何额外的空间,对吗?
  • @TerryTsao 是的。就是这样。
  • 另一个后续问题,对于函子的情况,与默认删除的情况相比,它不会产生任何额外的空间或时间,我想对了吗?
  • @TerryTsao 没错。与std::default_delete&lt;B&gt; 相比,DeleteB 不需要额外的空间或时间。事实上,std::default_delete 的定义非常相似。
猜你喜欢
  • 2022-12-13
  • 1970-01-01
  • 2020-03-13
  • 1970-01-01
  • 1970-01-01
  • 2012-11-05
  • 2013-09-25
  • 2017-09-27
  • 2013-04-12
相关资源
最近更新 更多