unique_ptr 和前向声明:编写工厂函数的正确方法
unique_ptr and forward declaration: the proper way to code a factory function
最近学了smart ptrs,想写一个returnsunique_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
行毫无意义的事实。
我遇到了同样的不完整类型错误。
以上两种情况本质上是一样的。经过一番搜索,我想我明白了错误背后的原因,
但我需要你们的一些指点(双关语)和确认。
- 删除不完整的类型很麻烦。这就是为什么禁止使用默认删除器创建 unique_ptrs 带有不完整类型的原因。
- 如果我使用自定义删除器,我应该可以做到这一点。
- 我猜是因为我在我的案例中使用的是默认删除器,由于某些我不太确定的原因我无法完成。
明确定义创建和销毁函数应该可以解决问题。但对我来说,这很丑陋。其一,默认删除器就可以解决我的问题。
另一方面,在我看来我不能将 lambda 用于驱逐舰,因为 lambda 的类型只有编译器知道,
而且我不能用 decltype
.
做我的工厂函数声明
所以我的问题是:
- 这次失败的原因是什么?
- 编写 returns unique_ptrs 的工厂函数的正确方法是什么?
如果我说的不对请指正。任何指针将不胜感激。
当编译器实例化std::unique_ptr<Foo>
的析构函数时,编译器必须找到Foo::~Foo()
并调用它。这意味着 Foo
在 std::unique_ptr<Foo>
被销毁的地方必须是一个完整的类型。
这段代码没问题:
struct Foo;
std::unique_ptr<Foo> create();
...只要不用调用std::unique_ptr<Foo>
的析构函数即可!对于 returns 一个 std::unique_ptr
到一个 class 的工厂函数,class 需要是一个完整的类型。这就是您声明工厂的方式:
#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);
}
这里到底发生了什么?
- 我们正在构建一个
std::unique_ptr<B>
来初始化 b
。
b
是一个局部变量,这意味着它的析构函数将在作用域的末尾被调用。
B
在实例化std::unique_ptr<B>
的析构函数时必须是完整类型。
B
是一个不完整的类型,所以我们不能销毁 b
.
好的,所以如果 B
是不完整的类型,则不能传递 std::unique_ptr<B>
。这种限制是有道理的。 pimpl 的意思是 "pointer to implementation"。外部代码访问 A
的实现没有意义,因此 A::b
应该是私有的。如果你必须访问 A::b
那么这不是粉刺,这是别的东西。
如果您确实必须在隐藏 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)class,这可能会有点烦人,因为您无法定义模板。这就是您的做法:
// a.hpp
struct DeleteB {
void operator()(B *) const noexcept;
};
// a.cpp
void DeleteB::operator()(B *b) const noexcept {
delete b;
}
定义自定义删除器可能是最好的选择,但如果我是你,我会找到一种方法来避免需要从 class.
外部访问实现细节
最近学了smart ptrs,想写一个returnsunique_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
行毫无意义的事实。
我遇到了同样的不完整类型错误。
以上两种情况本质上是一样的。经过一番搜索,我想我明白了错误背后的原因, 但我需要你们的一些指点(双关语)和确认。
- 删除不完整的类型很麻烦。这就是为什么禁止使用默认删除器创建 unique_ptrs 带有不完整类型的原因。
- 如果我使用自定义删除器,我应该可以做到这一点。
- 我猜是因为我在我的案例中使用的是默认删除器,由于某些我不太确定的原因我无法完成。
明确定义创建和销毁函数应该可以解决问题。但对我来说,这很丑陋。其一,默认删除器就可以解决我的问题。
另一方面,在我看来我不能将 lambda 用于驱逐舰,因为 lambda 的类型只有编译器知道,
而且我不能用 decltype
.
所以我的问题是:
- 这次失败的原因是什么?
- 编写 returns unique_ptrs 的工厂函数的正确方法是什么?
如果我说的不对请指正。任何指针将不胜感激。
当编译器实例化std::unique_ptr<Foo>
的析构函数时,编译器必须找到Foo::~Foo()
并调用它。这意味着 Foo
在 std::unique_ptr<Foo>
被销毁的地方必须是一个完整的类型。
这段代码没问题:
struct Foo;
std::unique_ptr<Foo> create();
...只要不用调用std::unique_ptr<Foo>
的析构函数即可!对于 returns 一个 std::unique_ptr
到一个 class 的工厂函数,class 需要是一个完整的类型。这就是您声明工厂的方式:
#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);
}
这里到底发生了什么?
- 我们正在构建一个
std::unique_ptr<B>
来初始化b
。 b
是一个局部变量,这意味着它的析构函数将在作用域的末尾被调用。B
在实例化std::unique_ptr<B>
的析构函数时必须是完整类型。B
是一个不完整的类型,所以我们不能销毁b
.
好的,所以如果 B
是不完整的类型,则不能传递 std::unique_ptr<B>
。这种限制是有道理的。 pimpl 的意思是 "pointer to implementation"。外部代码访问 A
的实现没有意义,因此 A::b
应该是私有的。如果你必须访问 A::b
那么这不是粉刺,这是别的东西。
如果您确实必须在隐藏 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)class,这可能会有点烦人,因为您无法定义模板。这就是您的做法:
// a.hpp
struct DeleteB {
void operator()(B *) const noexcept;
};
// a.cpp
void DeleteB::operator()(B *b) const noexcept {
delete b;
}
定义自定义删除器可能是最好的选择,但如果我是你,我会找到一种方法来避免需要从 class.
外部访问实现细节