具有不完整类型的std :: unique_ptr将无法编译

时间:2012-03-31 09:03:17

标签: c++ unique-ptr incomplete-type libc++

我正在使用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 ++中的错误还是我在这里做错了什么?

7 个答案:

答案 0 :(得分:210)

以下是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_;

答案 1 :(得分:39)

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

答案 2 :(得分:13)

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

确保在.h类窗口中只有函数声明。窗口的所有函数体必须位于.cpp文件中。而对于window_impl也是......

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

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

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

必须只是一个声明:

  class window {
    virtual ~window();
  }

答案 3 :(得分:12)

使用自定义删除器

问题是unique_ptr<T>必须在自己的析构函数,移动赋值运算符和T::~T()成员函数(仅)中调用析构函数unique_ptr::reset()。但是,必须在几个PIMPL情境中调用(隐式或显式)(已经在外部类的析构函数和移动赋值运算符中)。

正如另一个答案中已经指出的那样,避免这种情况的一种方法是将需要unique_ptr::~unique_ptr()unique_ptr::operator=(unique_ptr&&)unique_ptr::reset()所有操作移入实际定义了pimpl助手类的源文件。

然而,这是相当不方便的,并且在某种程度上违背了pimpl idoim的要点。一个更清晰的解决方案,避免使用自定义删除器,只将其定义移动到丘疹助手类所在的源文件中。这是一个简单的例子:

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

您可以使用免费函数或static foo成员与lambda一起使用而不是单独的删除类:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};

答案 4 :(得分:1)

要添加到其他关于自定义删除工具的回复中,请在我们的内部&#34;实用程序库&#34;我添加了一个帮助程序头来实现这个常见模式(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

答案 5 :(得分:1)

可能不是最佳解决方案,但是有时您可以使用shared_ptr。 如果这当然有点矫kill过正,但是……对于unique_ptr,我也许要再等10年,直到C ++标准制定者决定使用lambda作为删除器。

另一面。 根据您的代码可能会发生,在销毁阶段window_impl将是不完整的。这可能是行为不确定的原因。 看到这个: Why, really, deleting an incomplete type is undefined behaviour?

因此,如果可能的话,我将使用虚拟析构函数为所有对象定义一个非常基础的对象。而且您几乎不错。您只需要记住,系统将为指针调用虚拟析构函数,因此您应该为每个祖先定义它。您还应该在继承部分将基类定义为虚拟类(有关详细信息,请参见this)。

答案 6 :(得分:0)

使用extern template

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

此外,为了便于使用,定义了一个类型opaque_ptr,它结合了unique_ptropaque_delete,类似于std::make_unique,我们定义了make_opaque。< /p>

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