web-dev-qa-db-ja.com

Tの完全な定義を知るためにstd :: unique_ptr <T>は必要ですか?

ヘッダーには次のようなコードがあります。

#include <memory>

class Thing;

class MyClass
{
    std::unique_ptr< Thing > my_thing;
};

Thing型定義を含まないcppにこのヘッダーを含めると、VS2010-SP1でコンパイルされません。

1> C:\ Program Files(x86)\ Microsoft Visual Studio 10.0\VC\include\memory(2067):エラーC2027:未定義のタイプ 'Thing'の使用

std::unique_ptrstd::shared_ptrに置き換えてコンパイルします。

したがって、完全な定義を必要とするのは現在のVS2010 std::unique_ptrの実装であり、完全に実装依存であると推測しています。

またはそれは? std::unique_ptrの実装が前方宣言のみで機能することを不可能にする標準要件に何かがありますか? Thingへのポインタのみを保持する必要があるため、奇妙に感じます。

230
Klaim

here から採用。

C++標準ライブラリのほとんどのテンプレートでは、完全な型でインスタンス化する必要があります。ただし、shared_ptrunique_ptrpartial例外です。すべてではありませんが、一部のメンバーは不完全な型でインスタンス化できます。これの動機は、未定義の動作を危険にさらすことなく、スマートポインターを使用して pimpl などのイディオムをサポートすることです。

未定義の動作は、不完全な型があり、deleteを呼び出すと発生する可能性があります。

class A;
A* a = ...;
delete a;

上記は有効なコードです。コンパイルされます。コンパイラは、上記のような上記のコードに対して警告を発する場合としない場合があります。実行すると、おそらく悪いことが起こります。運がよければ、プログラムはクラッシュします。しかし、より可能性の高い結果は、~A()が呼び出されないため、プログラムが静かにメモリをリークすることです。

上記の例でauto_ptr<A>を使用しても役に立ちません。生のポインタを使用した場合と同じ未定義の動作が引き続き発生します。

それにもかかわらず、特定の場所で不完全なクラスを使用することは非常に便利です! shared_ptrおよびunique_ptrが役立つ場所です。これらのスマートポインターのいずれかを使用すると、完全な型が必要な場合を除き、不完全な型を回避できます。そして最も重要なのは、完全な型が必要なときに、その時点で不完全な型でスマートポインターを使用しようとすると、コンパイル時エラーが発生することです。

これ以上未定義の動作はありません:

コードがコンパイルされる場合、必要なすべての場所で完全な型を使用しています。

class A
{
    class impl;
    std::unique_ptr<impl> ptr_;  // ok!

public:
    A();
    ~A();
    // ...
};

shared_ptrおよびunique_ptrは、異なる場所に完全な型が必要です。理由は不明であり、動的削除者と静的削除者に関係しています。正確な理由は重要ではありません。実際、ほとんどのコードでは、完全な型が必要な場所を正確に知ることはあまり重要ではありません。コーディングするだけで、間違えた場合は、コンパイラが通知します。

ただし、参考になる場合は、完全性の要件に関してshared_ptrおよびunique_ptrのいくつかのメンバーを記載した表を次に示します。メンバーが完全な型を必要とする場合、エントリには「C」が含まれます。それ以外の場合、テーブルエントリには「I」が入力されます。

Complete type requirements for unique_ptr and shared_ptr

                            unique_ptr       shared_ptr
+------------------------+---------------+---------------+
|          P()           |      I        |      I        |
|  default constructor   |               |               |
+------------------------+---------------+---------------+
|      P(const P&)       |     N/A       |      I        |
|    copy constructor    |               |               |
+------------------------+---------------+---------------+
|         P(P&&)         |      I        |      I        |
|    move constructor    |               |               |
+------------------------+---------------+---------------+
|         ~P()           |      C        |      I        |
|       destructor       |               |               |
+------------------------+---------------+---------------+
|         P(A*)          |      I        |      C        |
+------------------------+---------------+---------------+
|  operator=(const P&)   |     N/A       |      I        |
|    copy assignment     |               |               |
+------------------------+---------------+---------------+
|    operator=(P&&)      |      C        |      I        |
|    move assignment     |               |               |
+------------------------+---------------+---------------+
|        reset()         |      C        |      I        |
+------------------------+---------------+---------------+
|       reset(A*)        |      C        |      C        |
+------------------------+---------------+---------------+

ポインター変換を必要とする操作には、unique_ptrshared_ptrの両方の完全な型が必要です。

unique_ptr<A>{A*}コンストラクターは、コンパイラーが~unique_ptr<A>()の呼び出しをセットアップする必要がない場合にのみ、不完全なAで回避できます。たとえば、unique_ptrをヒープに配置すると、不完全なAで逃げることができます。この点に関する詳細は BarryTheHatchet's answer here にあります。

311
Howard Hinnant

コンパイラーは、MyClassのデフォルトのデストラクターを生成するために、Thingの定義を必要とします。デストラクタを明示的に宣言し、その(空の)実装をCPPファイルに移動すると、コードがコンパイルされます。

41
Igor Nazarenko

これは実装依存ではありません。動作する理由は、shared_ptrが実行時に呼び出す正しいデストラクタを決定するためです。これは型シグネチャの一部ではありません。ただし、unique_ptrのデストラクタisはその型の一部であり、コンパイル時に認識される必要があります。

14
Puppy

デフォルトのコンストラクター(またはデストラクター)が問題であるのに、cppで宣言された空のコンストラクターが問題ではない理由は、現在の回答が正確に特定していないようです。

何が起こっているのですか:

外部クラス(つまりMyClass)にコンストラクターまたはデストラクタがない場合、コンパイラーはデフォルトのクラスを生成します。これに伴う問題は、コンパイラが基本的に.hppファイルにデフォルトの空のコンストラクタ/デストラクタを挿入することです。つまり、デフォルトのコンストラクタ/デストラクタのコードは、ライブラリのバイナリではなく、ホスト実行可能ファイルのバイナリとともにコンパイルされます。ただし、この定義では実際には部分クラスを構築できません。そのため、リンカがライブラリのバイナリに入り、コンストラクタ/デストラクタを取得しようとしても、リンカが見つからず、エラーが発生します。コンストラクタ/デストラクタコードが.cppにあった場合、ライブラリバイナリはリンクに使用できます。

これはunique_ptrまたはshared_ptrを使用することとは関係がなく、unique_ptr実装の古いVC++のバグを混乱させる可能性があるようです(VC++ 2015は私のマシンで正常に動作します)。

話の教訓は、ヘッダーにコンストラクター/デストラクターの定義がないことです。宣言のみを含めることができます。たとえば、hppの~MyClass()=default;は機能しません。コンパイラがデフォルトのコンストラクタまたはデストラクタを挿入できるようにすると、リンカーエラーが発生します。

もう1つの補足事項:cppファイルにコンストラクターとデストラクタを使用した後でもこのエラーが引き続き発生する場合は、おそらくライブラリが適切にコンパイルされていないことが原因です。たとえば、VC++でプロジェクトタイプをコンソールからライブラリに変更しただけで、VC++が_LIBプリプロセッサシンボルを追加せず、まったく同じエラーメッセージを生成したため、このエラーが発生しました。

6
Shital Shah

完全を期すために:

ヘッダー:A.h

class B; // forward declaration

class A
{
    std::unique_ptr<B> ptr_;  // ok!  
public:
    A();
    ~A();
    // ...
};

ソースA.cpp:

class B {  ...  }; // class definition

A::A() { ... }
A::~A() { ... }

クラスBの定義は、コンストラクタ、デストラクタ、および暗黙的にBを削除する可能性のあるものによって確認する必要があります(コンストラクタは上記のリストに表示されませんが、VS2017では、コンストラクタでもBの定義が必要です。コンストラクターで例外が発生した場合、unique_ptrは再び破棄されます。)

2
Joachim

テンプレートのインスタンス化の時点で、Thingの完全な定義が必要です。これが、pimplイディオムがコンパイルされる正確な理由です。

それが可能でなかった場合、人々は this のような質問をしません。

1
BЈовић

簡単な答えは、代わりにshared_ptrを使用することです。

0
deltanine