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


202

我在使用pimpl惯用语std::unique_ptr

class window {
  window(const rectangle& rect);

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

但是,我在第304行中遇到了有关使用不完整类型的编译错误<memory>

无效的将' sizeof'应用于不完整的类型' uixx::window::window_impl'

据我所知,std::unique_ptr应该可以使用不完整的类型。这是libc ++中的错误,还是我在这里做错了?


完整性要求的参考链接:stackoverflow.com/a/6089065/576911
Howard Hinnant 2012年

1
从那时起,通常会构造一个Pimpl,并且不对其进行修改。我通常使用std :: shared_ptr <const
window_impl

相关:我非常想知道为什么它在MSVC中有效,以及如何防止它起作用(这样我才不会破坏我的GCC同事的著作)。
Len

Answers:


258

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

3
我发现您的第一个解决方案(添加foo析构函数)允许类声明本身进行编译,但是在任何地方声明该类型的对象都会导致原始错误(“'sizeof'...的无效应用程序”)。
Jeff Trull 2012年

38
很好的答案,仅需注意;我们仍然可以通过将例如放置foo::~foo() = default;在src文件中来使用默认构造函数/析构函数
assem 2014年

2
使用模板构造函数的一种方法是在类主体中声明但不定义构造函数,在可以看到完整的impl定义的地方定义它,并在那里显式实例化所有必要的实例。
enobayram 2015年

2
您能解释一下在某些情况下该方法如何工作,而在另一些情况下该方法不工作吗?我已经使用了的unique_ptr并没有析构函数类PIMPL方法,而在另一个项目中我的代码失败并提到的错误OP编译..
好奇的

1
似乎如果在c ++ 11样式的类的头文件中将unique_ptr的默认值设置为{nullptr},则由于上述原因,还需要完整的声明。
feirainy

53

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

请注意,使用定制删除功能无法使用一些std::make_unique(可以从C ++ 14),如已经讨论这里


6
就我而言,这是正确的解决方案。它不是使用pimpl惯用语的唯一方法,而是将std :: unique_ptr与不完整的类一起使用是一个普遍的问题。std :: unique_ptr <X>使用的默认删除器尝试执行“删除X”,如果X是前向声明,则无法执行。通过指定删除器功能,可以将该功能放入完全定义了类X的源文件中。然后,其他源文件也可以使用std :: unique_ptr <X,DeleterFunc>,即使X只是前向声明,只要它们与包含DeleterFunc的源文件链接即可。
谢尔顿

1
当您必须具有内联函数定义来创建“ Foo”类型的实例(例如,引用构造函数和析构函数的静态“ getInstance”方法)并且您不想将它们移入实现文件时,这是一个很好的解决方法如@ adspx5所建议。
GameSalutes

20

使用自定义删除器

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

作为另一个答案已经指出,要避免一个办法是将所有需要的操作unique_ptr::~unique_ptr()unique_ptr::operator=(unique_ptr&&)以及unique_ptr::reset()为在平普尔辅助类实际上是定义的源文件。

然而,这是相当不便的,并且在一定程度上违背了pimpl idoim的要点。一个更干净的解决方案,它避免了使用自定义删除器的所有操作,而仅将其定义移到pimple 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; }

除了单独的删除器类,您还可以将自由函数或static成员foo与lambda结合使用:

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

15

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

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

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

但是您不能将空的dtor正文放入头文件中:

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

必须只是一个声明:

  class window {
    virtual ~window();
  }

这也是我的解决方案。方式更简洁。只需在标头中声明并在cpp文件中定义构造函数/析构函数即可。
克里斯·莫尼斯

2

为了增加关于自定义删除器的答复,在我们内部的“实用程序库”中,我添加了一个辅助标头来实现此通用模式(std::unique_ptr类型不完整,只有某些TU才知道,例如,避免长编译时间或提供对客户来说只是一个不透明的句柄)。

它提供了这种模式的通用框架:一个定制的deleteer类,该类调用一个外部定义的deleter函数;unique_ptr此deleter类的a类型别名;一个宏,用于在TU中声明deleter函数,该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

1

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

另一边。根据您的代码可能会发生,在销毁阶段window_impl将是不完整的。这可能是行为不确定的原因。看到以下内容: 为什么删除一个不完整的类型确实是未定义的行为?

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.