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


203

私はpimpl-idiomを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

1
pimplはしばしば構築され、それ以降変更されません。私は通常std :: shared_ptr <const window_impl>を使用します
mfnx 2018年

関連:これがMSVCで機能する理由と、それが機能しないようにする方法を知りたいです(GCCの同僚のコンパイルを壊さないようにするため)。
レン

回答:


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」の無効なアプリケーション...)。
ジェフトゥルル2012

38
注意してください。たとえばfoo::~foo() = default;srcファイルに配置することで、デフォルトのコンストラクタ/デストラクタを引き続き使用できます
assem

2
テンプレートコンストラクターを使用する1つの方法は、クラス本体でコンストラクターを宣言して定義せずに、完全な実装定義が見られる場所で定義し、そこで必要なすべてのインスタンス化を明示的にインスタンス化することです。
enobayram 2015

2
これがどのように機能し、他のケースでは機能しないかを説明できますか?..私はノーデストラクタとunique_ptrをし、クラスでPIMPLイディオムを使用しており、別のプロジェクトに私のコードは、OPが言及したエラーでコンパイルに失敗する
おさるの

1
c ++ 11スタイルのクラスのヘッダーファイルでunique_ptrのデフォルト値が{nullptr}に設定されている場合、上記の理由により完全な宣言も必要です。
feirainy

53

以下のようアレクサンドル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すでに説明したように、カスタムのDeleter関数を使用すると、(C ++ 14から利用可能)を使用できなくなります


6
これは、私にとっては正しい解決策です。これはpimpl-idiomの使用に固有のものではなく、不完全なクラスでstd :: unique_ptrを使用する場合の一般的な問題です。std :: unique_ptr <X>で使用されるデフォルトの削除機能は「Xの削除」を試みますが、Xが前方宣言の場合はできません。削除関数を指定すると、クラスXが完全に定義されているソースファイルにその関数を配置できます。XがDeleterFuncを含むソースファイルにリンクされている限り、Xは単なる前方宣言であっても、他のソースファイルはstd :: unique_ptr <X、DeleterFunc>を使用できます。
シェルトン、

1
これは、 "Foo"タイプのインスタンスを作成するインライン関数定義(たとえば、コンストラクターとデストラクターを参照する静的な "getInstance"メソッド)が必要であり、これらを実装ファイルに移動したくない場合の適切な回避策です。 @ adspx5が示唆するように。
GameSalutes 2016年

20

カスタム削除機能を使用する

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

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

ただし、これはかなり不便であり、ある程度の意味での略語の本質そのものに反しています。カスタムの削除機能を使用して、その定義をにきびヘルパークラスが存在するソースファイルにのみ移動することをすべて回避する、よりクリーンなソリューション。以下に簡単な例を示します。

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

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

15

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

クラスウィンドウの.h内に関数宣言のみがあることを確認してください。ウィンドウのすべての関数本体は.cppファイルになければなりません。そしてwindow_implについても...

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

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

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

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

  class window {
    virtual ~window();
  }

これも私の解決策でした。もっと簡潔に。コンストラクター/デストラクターをヘッダーで宣言し、cppファイルで定義するだけです。
クリスモーネス2018

2

カスタムの削除に関する他の返信に追加するために、内部の「ユーティリティライブラリ」にヘルパーヘッダーを追加して、この一般的なパターンを実装しました(std::unique_ptr不完全なタイプで、たとえば、長いコンパイル時間を回避したり、クライアントへの不透明なハンドル)。

これは、このパターンに共通の足場を提供します。外部で定義された削除関数を呼び出すカスタム削除クラス、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

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

別の側面。コードによっては、破壊ステージでwindow_implが不完全になる可能性があります。これが未定義の動作の理由である可能性があります。これを見てください: なぜ、本当に、不完全な型を削除することが未定義の振る舞いなのでしょうか?

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.