web-dev-qa-db-ja.com

不完全な型のstd :: unique_ptrはコンパイルされません

私は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行目に、不完全な型の使用に関するコンパイルエラーが表示されます。

不完全な型「uixx::window::window_impl」への「sizeof」の無効な適用

私の知る限り、std::unique_ptrは不完全な型で使用できるはずです。これはlibc ++のバグですか、ここで何か間違っていますか?

172
user1203803

不完全な型を持つ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_;
221
Alexandre C.

Alexandre C。 が述べたように、問題はwindow_implの型がまだ不完全な場所で暗黙的に定義されているwindowのデストラクタに帰着します。彼のソリューションに加えて、私が使用した別の回避策は、ヘッダーで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;
}

カスタム削除機能を使用

問題は、unique_ptr<T>がデストラクタT::~T()を独自のデストラクタ、移動割り当て演算子、およびunique_ptr::reset()メンバー関数(のみ)で呼び出す必要があることです。ただし、これらは(暗黙的または明示的に)いくつかのPIMPL状況(外部クラスのデストラクタおよび移動代入演算子で既に)で呼び出されなければなりません。

別の回答で既に指摘したように、それを回避する1つの方法は、unique_ptr::~unique_ptr()unique_ptr::operator=(unique_ptr&&)を必要とするall操作を移動することです。 unique_ptr::reset()は、pimplヘルパークラスが実際に定義されているソースファイルに入れます。

しかし、これはやや不便であり、ある程度まではmpl子の理想の要点を無視しています。 custom deleterを使用し、その定義をニキビヘルパークラスが存在するソースファイルに移動することだけをすべて回避する、よりクリーンなソリューション。以下に簡単な例を示します。

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

別の削除クラスの代わりに、フリー関数またはstaticfooメンバーをラムダと組み合わせて使用​​することもできます。

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

おそらく、不完全な型を使用するクラス内の.hファイル内にいくつかの関数本体があります。

クラスウィンドウの.h内には、関数宣言しかないことを確認してください。ウィンドウのすべての関数本体は、.cppファイルに存在する必要があります。また、window_implについても...

ところで、Windowsクラスのデストラクタ宣言を.hファイルに明示的に追加する必要があります。

ただし、空のdtor本文をヘッダーファイルに入れることはできません。

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

単なる宣言でなければなりません:

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

最善の解決策ではないかもしれませんが、代わりに shared_ptr を使用する場合があります。もちろんそれは少しやり過ぎですが、unique_ptrに関しては、C++標準メーカーがラムダを削除プログラムとして使用することを決定するまで、おそらく10年以上待つでしょう。

別の側面。コードごとに、破壊段階でwindow_implが不完全になることがあります。これは、未定義の動作の理由である可能性があります。これを参照してください: なぜ、本当に、不完全な型を削除するのは未定義の動作ですか?

したがって、可能であれば、仮想デストラクタを使用して、すべてのオブジェクトに非常に基本的なオブジェクトを定義します。そして、あなたはほとんど良いです。システムはポインタの仮想デストラクタを呼び出すことに注意してください。したがって、すべての先祖に対して定義する必要があります。また、継承セクションで基本クラスを仮想として定義する必要があります(詳細については this を参照してください)。

1

カスタム削除者に関する他の返信に追加するために、内部の「ユーティリティライブラリ」にヘルパーヘッダーを追加して、この一般的なパターン(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
1
Matteo Italia