我正在使用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++或我在这里做错了什么?


当前回答

使用extern模板

使用std::unique_ptr<T>(其中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);
}

此外,为了便于使用,定义一个组合了unique_ptr和opaque_delete的类型opaque_ptr,类似于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模板构造一起使用。这里有一个例子。

/* --- 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被实例化,所以代码编译时不会出现错误。为了让链接器开心,我们在foo.cpp中实例化了opaque_delete。

/* --- 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之后,所需的样板代码相当小。

其他回答

可能不是最好的解决方案,但有时可以使用shared_ptr代替。 当然这有点太夸张了,但是……至于unique_ptr,我可能要再等10年,直到c++标准制定者决定使用lambda作为删除器。

另一个方面。 根据你的代码,在销毁阶段,window_impl可能是不完整的。这可能是未定义行为的原因。 看到这个: 为什么,真的,删除一个不完整的类型是未定义的行为?

如果可能的话,我会用虚析构函数为所有对象定义一个非常基本的对象。你就快好了。你只需要记住,系统会为指针调用虚析构函数,所以你应该为每个祖先定义它。您还应该在继承部分中将基类定义为虚类(详细信息请参阅此部分)。

下面是一些带有不完整类型的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可用),正如这里已经讨论过的那样。

使用自定义删除器

问题是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);
};

使用extern模板

使用std::unique_ptr<T>(其中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);
}

此外,为了便于使用,定义一个组合了unique_ptr和opaque_delete的类型opaque_ptr,类似于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模板构造一起使用。这里有一个例子。

/* --- 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被实例化,所以代码编译时不会出现错误。为了让链接器开心,我们在foo.cpp中实例化了opaque_delete。

/* --- 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之后,所需的样板代码相当小。