我正在使用std::unique_ptr丘疹成语:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

然而,我得到了一个关于使用不完整类型的编译错误,在<memory>中的304行:

对不完整类型uixx::window::window_impl的sizeof应用无效

据我所知,std::unique_ptr应该能够与不完整类型一起使用。这是一个错误在libc++或我在这里做错了什么?


当前回答

使用自定义删除器

问题是unique_ptr<T>必须在它自己的析构函数、它的move赋值操作符和unique_ptr::reset()成员函数中调用析构函数T::~T()。但是,在几种PIMPL情况下(已经在外部类的析构函数和move赋值操作符中)必须调用这些函数(隐式或显式)。

正如在另一个回答中已经指出的,避免这种情况的一种方法是将所有需要unique_ptr::~unique_ptr()、unique_ptr::operator=(unique_ptr&&)和unique_ptr::reset()的操作移动到实际定义pimpl helper类的源文件中。

然而,这是相当不方便的,在某种程度上违背了皮条客习俗的要点。一个更干净的解决方案是使用一个自定义删除器,只将它的定义移动到粉刺助手类所在的源文件中。这里有一个简单的例子:

// 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; }

除了单独的delete类,你也可以使用foo的free函数或static成员:

class foo {
    struct pimpl;
    static void delete_pimpl(pimpl*);
    using deleter = void(&)(pimpl*);
    std::unique_ptr<pimpl,deleter> m_pimpl;
  public:
    foo(some data);
};

其他回答

下面是一些带有不完整类型的std::unique_ptr的例子。问题在于破坏。

如果你在unique_ptr中使用pimpl,你需要声明一个析构函数:

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;
}

注意,使用自定义delete函数排除了使用std::make_unique(从c++ 14可用),正如这里已经讨论过的那样。

可能在类的.h文件中有一些使用不完全类型的函数体。

确保在.h for class窗口中只有函数声明。window的所有函数体必须在.cpp文件中。对于window_impl也是如此…

顺便说一句,你必须在你的.h文件中显式地为windows类添加析构函数声明。

但是你不能在头文件中放空的dtor主体:

class window {
    virtual ~window() {};
  }

肯定只是个宣言:

  class window {
    virtual ~window();
  }

使用自定义删除器

问题是unique_ptr<T>必须在它自己的析构函数、它的move赋值操作符和unique_ptr::reset()成员函数中调用析构函数T::~T()。但是,在几种PIMPL情况下(已经在外部类的析构函数和move赋值操作符中)必须调用这些函数(隐式或显式)。

正如在另一个回答中已经指出的,避免这种情况的一种方法是将所有需要unique_ptr::~unique_ptr()、unique_ptr::operator=(unique_ptr&&)和unique_ptr::reset()的操作移动到实际定义pimpl helper类的源文件中。

然而,这是相当不方便的,在某种程度上违背了皮条客习俗的要点。一个更干净的解决方案是使用一个自定义删除器,只将它的定义移动到粉刺助手类所在的源文件中。这里有一个简单的例子:

// 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; }

除了单独的delete类,你也可以使用foo的free函数或static成员:

class foo {
    struct pimpl;
    static void delete_pimpl(pimpl*);
    using deleter = void(&)(pimpl*);
    std::unique_ptr<pimpl,deleter> m_pimpl;
  public:
    foo(some data);
};

为了添加到对方的自定义删除器的回复,在我们的内部“实用程序库”中,我添加了一个helper头来实现这个通用模式(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