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

问题描述 投票:3回答:1

最近了解到智能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. 我猜是因为我在我的情况下使用默认删除器,因为某些原因我无法完成,我不太确定。

明确定义create和destroy函数应该可以解决问题。但对我来说,它很难看。首先,默认删除器将在我的情况下。另一方面,在我看来,我不能使用lambda作为驱逐舰,因为lambda的类型只有编译器知道,我不能用decltype做我的工厂函数声明。

所以我的问题是:

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

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

c++ c++11 smart-pointers forward-declaration
1个回答
2
投票

当编译器实例化std::unique_ptr<Foo>的析构函数时,编译器必须找到Foo::~Foo()并调用它。这意味着Foo必须是std::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的析构函数被实例化时,std::unique_ptr<B>必须是一个完整的类型。
  4. 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;
}

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

© www.soinside.com 2019 - 2024. All rights reserved.