[英]std::unique_ptr with an incomplete type won't compile
我在std::unique_ptr
使用 pimpl-idiom :
class window {
window(const rectangle& rect);
private:
class window_impl; // defined elsewhere
std::unique_ptr<window_impl> impl_; // won't compile
};
但是,我在<memory>
第 304 行收到有关使用不完整类型的编译错误:
“
sizeof
”对不完整类型“uixx::window::window_impl
”的无效应用
据我所知, std::unique_ptr
应该能够与不完整的类型一起使用。 这是 libc++ 中的错误还是我在这里做错了什么?
以下是一些类型不完整的std::unique_ptr
示例。 问题在于破坏。
如果将 pimpl 与unique_ptr
一起使用,则需要声明一个析构函数:
class foo
{
class impl;
std::unique_ptr<impl> impl_;
public:
foo(); // You may need a def. constructor to be defined elsewhere
~foo(); // Implement (with {}, or with = default;) where impl is complete
};
因为否则编译器会生成一个默认的,它需要一个完整的foo::impl
声明。
如果您有模板构造函数,那么即使您没有构造impl_
成员,您也会被搞砸:
template <typename T>
foo::foo(T bar)
{
// Here the compiler needs to know how to
// destroy impl_ in case an exception is
// thrown !
}
在命名空间范围内,使用unique_ptr
也不起作用:
class impl;
std::unique_ptr<impl> impl_;
因为编译器在这里必须知道如何销毁这个静态持续时间对象。 一种解决方法是:
class impl;
struct ptr_impl : std::unique_ptr<impl>
{
~ptr_impl(); // Implement (empty body) elsewhere
} impl_;
正如Alexandre C.提到的,问题归结为window
的析构函数被隐式定义在window_impl
的类型仍然不完整的地方。 除了他的解决方案之外,我使用的另一种解决方法是在标头中声明一个 Deleter 函子:
// Foo.h
class FooImpl;
struct FooImplDeleter
{
void operator()(FooImpl *p);
};
class Foo
{
...
private:
std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};
// Foo.cpp
...
void FooImplDeleter::operator()(FooImpl *p)
{
delete p;
}
请注意,使用自定义 Deleter 函数会排除使用std::make_unique
(可从 C++14 获得),如已在此处讨论的。
使用自定义删除器
问题是unique_ptr<T>
必须在其自身的析构函数、其移动赋值运算符和unique_ptr::reset()
成员函数(仅)中调用析构函数T::~T()
)。 但是,这些必须在几种 PIMPL 情况下(已经在外部类的析构函数和移动赋值运算符中)调用(隐式或显式)。
正如在另一个答案中已经指出的那样,避免这种情况的一种方法是将所有需要unique_ptr::~unique_ptr()
、 unique_ptr::operator=(unique_ptr&&)
和unique_ptr::reset()
到源文件中pimpl helper 类实际上是定义的。
然而,这相当不方便,并且在某种程度上违背了 pimpl idoim 的要点。 一个更简洁的解决方案,它避免了使用自定义删除器的所有问题,并且只将其定义移动到 pimple 帮助程序类所在的源文件中。 这是一个简单的例子:
// file.h
class foo
{
struct pimpl;
struct pimpl_deleter { void operator()(pimpl*) const; };
std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
foo(some data);
foo(foo&&) = default; // no need to define this in file.cc
foo&operator=(foo&&) = default; // no need to define this in file.cc
//foo::~foo() auto-generated: no need to define this in file.cc
};
// file.cc
struct foo::pimpl
{
// lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }
除了单独的删除器类,您还可以将foo
的自由函数或static
成员与 lambda 结合使用:
class foo {
struct pimpl;
static void delete_pimpl(pimpl*);
std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};
可能您在使用不完整类型的类中的 .h 文件中有一些函数体。
确保在类窗口的 .h 中只有函数声明。 window 的所有函数体都必须在 .cpp 文件中。 对于 window_impl 也是...
顺便说一句,您必须在 .h 文件中为 windows 类显式添加析构函数声明。
但是您不能在头文件中放入空的 dtor 主体:
class window {
virtual ~window() {};
}
必须只是一个声明:
class window {
virtual ~window();
}
为了添加其他关于自定义删除器的回复,在我们内部的“实用程序库”中,我添加了一个帮助程序头来实现这个常见模式(不完整类型的std::unique_ptr
,只有一些 TU 知道,例如避免长时间编译次或仅为客户提供一个不透明的句柄)。
它为这个模式提供了通用的脚手架:一个调用外部定义的删除器函数的自定义删除器类,一个带有这个删除器类的unique_ptr
的类型别名,以及一个在具有完整定义的 TU 中声明删除器函数的宏方式。 我认为这有一些普遍的用处,所以这里是:
#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>
/**
Helper to define a `std::unique_ptr` that works just with a forward
declaration
The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
available, as it has to emit calls to `delete` in every TU that may use it.
A workaround to this problem is to have a `std::unique_ptr` with a custom
deleter, which is defined in a TU that knows the full definition of `T`.
This header standardizes and generalizes this trick. The usage is quite
simple:
- everywhere you would have used `std::unique_ptr<T>`, use
`czu::unique_opaque<T>`; it will work just fine with `T` being a forward
declaration;
- in a TU that knows the full definition of `T`, at top level invoke the
macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
by `czu::unique_opaque<T>`
*/
namespace czu {
template<typename T>
struct opaque_deleter {
void operator()(T *it) {
void opaque_deleter_hook(T *);
opaque_deleter_hook(it);
}
};
template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}
/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }
#endif
可能不是最好的解决方案,但有时您可以使用shared_ptr代替。 如果这当然有点矫枉过正,但是……至于 unique_ptr,我可能还要等 10 年,直到 C++ 标准制定者决定使用 lambda 作为删除器。
另一边。 根据您的代码,可能会发生在销毁阶段 window_impl 将不完整的情况。 这可能是未定义行为的原因。 看到这个: 为什么,真的,删除一个不完整的类型是未定义的行为?
因此,如果可能的话,我会使用虚拟析构函数为所有对象定义一个非常基础的对象。 你几乎好了。 您只需要记住系统将为您的指针调用虚拟析构函数,因此您应该为每个祖先定义它。 您还应该在继承部分将基类定义为虚拟(有关详细信息,请参阅此)。
extern template
在T
是不完整类型的情况下使用std::unique_ptr<T>
的问题是unique_ptr
需要能够为各种操作删除T
的实例。 类unique_ptr
使用std::default_delete<T>
删除实例。 因此,在一个理想的世界,我们只是写
extern template class std::default_delete<T>;
防止std::default_delete<T>
被实例化。 然后,声明
template class std::default_delete<T>;
在T
完成的地方,将实例化模板。
这里的问题是default_delete
实际上定义了不会被实例化的内联方法。 所以,这个想法行不通。 但是,我们可以解决这个问题。
首先,让我们定义一个不内联调用运算符的删除器。
/* --- opaque_ptr.hpp ------------------------------------------------------- */
#ifndef OPAQUE_PTR_HPP_
#define OPAQUE_PTR_HPP_
#include <memory>
template <typename T>
class opaque_delete {
public:
void operator() (T* ptr);
};
// Do not move this method into opaque_delete, or it will be inlined!
template <typename T>
void opaque_delete<T>::operator() (T* ptr) {
std::default_delete<T>()(ptr);
}
此外,为了便于使用,定义了一个类型opaque_ptr
,它结合了unique_ptr
和opaque_delete
,类似于std::make_unique
,我们定义了make_opaque
。
/* --- opaque_ptr.hpp cont. ------------------------------------------------- */
template <typename T>
using opaque_ptr = std::unique_ptr<T, opaque_delete<T>>;
template<typename T, typename... Args>
inline opaque_ptr<T> make_opaque(Args&&... args)
{
return opaque_ptr<T>(new T(std::forward<Args>(args)...));
}
#endif
opaque_delete
类型现在可以与extern template
构造一起使用。 这是一个例子。
/* --- foo.hpp -------------------------------------------------------------- */
#ifndef FOO_HPP_
#define FOO_HPP_
#include "opaque_ptr.hpp"
class Foo {
public:
Foo(int n);
void print();
private:
struct Impl;
opaque_ptr<Impl> m_ptr;
};
// Do not instantiate opaque_delete.
extern template class opaque_delete<Foo::Impl>;
#endif
由于我们防止opaque_delete
被实例化,因此代码编译时不会出错。 为了使链接开心,我们实例opaque_delete
我们foo.cpp
。
/* --- foo.cpp -------------------------------------------------------------- */
#include "foo.hpp"
#include <iostream>
struct Foo::Impl {
int n;
};
// Force instantiation of opaque_delete.
template class opaque_delete<Foo::Impl>;
其余的方法可以如下实现。
/* --- foo.cpp cont. -------------------------------------------------------- */
Foo::Foo(int n)
: m_ptr(new Impl)
{
m_ptr->n = n;
}
void Foo::print() {
std::cout << "n = " << m_ptr->n << std::endl;
}
这种解决方案的优点是,一旦定义了opaque_delete
,所需的样板代码就相当小了。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.