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

unique_ptr and forward declaration: the proper way to code a factory function

本文关键字:函数 工厂 方法 ptr 声明 unique      更新时间:2023-10-16

最近学习了智能 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痘痘成语。

// 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的工厂函数的正确方法是什么?

如果我说的任何话是错误的,请纠正我。任何指示将不胜感激。

当编译器实例化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。您必须在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<B>来初始化b
  2. b是一个局部变量,这意味着它的析构函数将在作用域结束时调用。
  3. 实例
  4. std::unique_ptr<B>的析构函数时,B必须是完整类型。
  5. B是一个不完整的类型,所以我们不能破坏b

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

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

std::shared_ptr<B>.这会以多态方式删除对象,以便在实例化std::shared_ptr<B>的析构函数时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;
}

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