コンピュータサイエンスにおいて、スマートポインタは、自動メモリ管理や境界チェックなどの追加機能を提供しながらポインタをシミュレートする抽象データ型です。このような機能は、効率性を維持しながら、ポインタの誤用によって引き起こされるバグを減らすことを目的としています。スマートポインタは通常、指し示すメモリを追跡し、ネットワーク接続やファイルハンドルなどの他のリソースの管理にも使用できます。スマートポインタは、1990年代前半にプログラミング言語C++で、C++の自動ガベージコレクションの欠如に対する批判への反論として初めて普及しました。[ 1 ] [ 2 ]生ポインタを避け、所有権を使用してライフタイムを決定するRustにもスマートポインタがあります。
ポインタの誤用は、バグの大きな原因となる可能性があります。スマートポインタは、メモリの解放を自動化することで、ほとんどのメモリリークを防ぎます。より一般的には、オブジェクトの破棄を自動化します。スマートポインタによって制御されるオブジェクトは、例えば所有者がローカル変数であり、実行がその変数のスコープを抜ける場合など、オブジェクトの最後の(または唯一の)所有者が破棄されると、自動的に破棄されます(ファイナライズされてから解放されます)。スマートポインタはまた、オブジェクトが使用されなくなるまで破棄を延期することで、ダングリングポインタを排除します。
言語が自動ガベージコレクションをサポートしている場合(例えば、JavaやC#)、メモリ管理の再利用や安全性に関してはスマートポインタは不要ですが、キャッシュデータ構造の常駐管理や、ファイルハンドルやネットワークソケットなどのオブジェクトのリソース管理といった他の目的には役立ちます。JavaにはポインタAPIはありませんが、参照を扱うための同様の概念があります。
スマートポインタにはいくつかの種類が存在する。参照カウントを用いるものもあれば、オブジェクトの所有権を1つのポインタに割り当てるものもある。
C++ はスマート ポインタの概念、特に参照カウント方式を普及させましたが、[ 3 ] C++ の設計に影響を与えた言語の 1 つは、参照カウント方式の参照が言語に組み込まれていました。C++ はSimula67から部分的に影響を受けています。[ 4 ] Simula67 の祖先はSimula Iです。Simula I の要素が C++ のnullを含まないポインタに類似している限り、また Simula I のアクティビティ本体としてダミー ステートメントを持つプロセスが C++ の構造体(それ自体は1960 年代当時のCAR Hoareのレコードに類似) に類似している限り、Simula I は遅くとも 1965 年 9 月までにプロセス (レコード) への参照カウント方式の要素 (間接参照を格納するポインタ式) を持っていました。これは以下の引用段落に示されています。[ 5 ]
プロセスは個別に参照できます。物理的には、プロセス参照は、プロセスにローカルなデータと、その現在の実行状態を定義する追加情報を含むメモリ領域へのポインタです。ただし、セクション 2.2 で述べた理由により、プロセス参照は常に要素と呼ばれる項目を介して間接的に行われます。正式には、プロセスへの参照は、要素型の式の値です。 … 要素の値は、要素変数への代入と参照、およびその他の手段によって格納および取得できます。 この言語には、プロセスの属性を外部、つまり他のプロセス内からアクセスできるようにするメカニズムが含まれています。これはリモート アクセスと呼ばれます。したがって、プロセスは参照可能なデータ構造です。
活動本体がダミーステートメントであるプロセスと、CAR HoareとN. Wirth が最近提案したレコード概念との類似性に注目する価値がある。
C++は、プロセス/レコードを割り当てる際に新しい要素を取得するために新しいキーワードを使用するという、Simulaのメモリ割り当てのアプローチを借用したため、 C++が最終的に要素内でSimulaの参照カウント式スマートポインタ機構を復活させたことは驚くべきことではない。
C++では、スマートポインタは、演算子オーバーロードによって従来の(生の)ポインタの動作(例えば、逆参照、代入)を模倣しつつ、追加のメモリ管理機能を提供するテンプレートクラスとして実装されます。
スマートポインタは、ポインタの参照先のメモリがどのように管理されるかを型で表現することで、意図的なプログラミングを容易にします。例えば、C++関数がポインタを返す場合、呼び出し元が情報を使い終えたときに参照先のメモリを解放すべきかどうかを知る方法はありません。
X * ambiguousFunction (); // 結果に対して何をするべきか?従来、命名規則は曖昧さを解消するために使用されてきましたが、[ 6 ]これはエラーが発生しやすく、手間のかかるアプローチです。C ++11 では、std::unique_ptr関数がを返すことを宣言することで、この場合の正しいメモリ管理を保証する方法が導入されました。
std :: unique_ptrを使用します。unique_ptr < X > obviousFunction ();関数の戻り値の型を として宣言することでstd::unique_ptr、呼び出し元が結果の所有権を取得することが明示され、C++ ランタイムはメモリが自動的に解放されることを保証します。C++11 で が導入される前はstd::unique_ptr、std::auto_ptrクラス が使用されていました (最終的にC++17で削除されました)。
C++には、以下のスマートポインタが用意されています。
std::auto_ptr<T>(C++11で非推奨となり、C++17で削除されました)[ 7 ]、「自動ポインタ」std::unique_ptr<T, Del>[ 8 ]、「一意のポインタ」std::shared_ptr<T>[ 9 ]、「共有ポインタ」std::weak_ptr<T>[ 10 ]、「弱いポインタ」C++26では、<hazard_pointer>ハザードポインタ( )という新しいポインタが追加されましたstd::hazard_pointer。[ 11 ]しかし、その名前とは裏腹に、スマート ポインタではなく、並行処理ツールです。これは、一度に最大 1 つのスレッドのみが所有できる、単一書き込みマルチ リーダー ポインタです。オブジェクトがハザード保護可能であるためには、 を継承する必要がありますstd::hazard_pointer_obj_base<T, Del>。ハザード ポインタは、以前から一部のサードパーティ ライブラリに存在していました。[ 12 ]
の割り当てを容易にするためにstd::shared_ptr<T>、C++11ではstd::make_shared<T>()ファクトリ関数が導入されました。
std :: shared_ptrを使用します。shared_ptr < X > s = std :: make_shared < X > ( /* コンストラクタパラメータをここに記述 */ );同様にstd::unique_ptr<T>、C++14 ではファクトリ関数が導入されましたstd::make_unique<T>()。
std :: unique_ptrを使用します。unique_ptr < X > u = std :: make_unique < X > ( /* コンストラクタパラメータをここに記述 */ );ほとんどの場合、newキーワード[ 13 ]よりもこれらのファクトリ関数を使用する方が好ましいですが、常にそれが可能とは限りません。
std::unique_ptr<T, Del>カスタム削除ロジックを追加するには、コンストラクタを手動で呼び出す必要があります。テンプレートパラメータ(カスタムデリータータイプを格納する)は、を介してstd::unique_ptr<T, Del>設定できないためです。Delstd::make_unique<T>()
using std :: FILE ; using std :: unique_ptr ;unique_ptr < FILE , decltype ( & std :: fclose ) > filePtr ( std :: fopen ( "data.txt" , "r" ), & std :: fclose );さらに、プライベート/プロテクトされたコンストラクタを呼び出すには、スマートポインタを手動で作成する必要があります。なぜなら、std::make_unique<T>()パブリックstd::make_shared<T>()コンストラクタしか呼び出せないからです。
std :: unique_ptrを使用します。class X { private : int n ;X ( ) = default ; public : static unique_ptr <X> create ( int n ) { return unique_ptr <X> ( new X ( n ) ) ; } } ;さらに、パラメータは中括弧ではなく括弧を介して渡されるため、スマートポインタを手動で作成することによってのみ、集約とコンストラクタを構築できstd::make_unique<T>()ます。std::make_shared<T>()std::initializer_list<T>
using std :: unique_ptr ; using std :: vector ;struct Matrix { vector < double > data ; size_t rows ; size_t cols ; };unique_ptr < Matrix > m ( new Matrix { . data = { 1.0 , 0.0 , 0.0 , 0.0 , 1.0 , 0.0 , 0.0 , 0.0 , 1.0 }, . rows = 3 , . cols = 3 });C++11std::unique_ptrでは、ヘッダーで定義されている が導入されました<memory>。[ 14 ]
Astd::unique_ptrは生ポインタを格納するコンテナであり、そのポインタはstd::unique_ptrが所有すると言われます。A はstd::unique_ptr、格納されているポインタのコピーを明示的に防止します (通常の代入ではコピーが発生します)。ただし、std::move関数を使用して、格納されているポインタの所有権を別の に譲渡することができますstd::unique_ptr。Astd::unique_ptrは、コピー コンストラクタと代入演算子が明示的に削除されているため、コピーできません。
import std ;std :: unique_ptrを使用します。unique_ptr < int > p1 ( new int ( 5 )); unique_ptr < int > p2 = p1 ; // コンパイルエラー。unique_ptr < int > p3 = std :: move ( p1 ); // 所有権を移転します。p3 がメモリを所有し、p1 は nullptr に設定されます。p3.reset (); // メモリを削除します。p1.reset ( ) ; //何も行いません。std::auto_ptrはC++11 で非推奨となり、 C++17で完全に削除されました。 のコピー コンストラクタと代入演算子は、std::auto_ptr格納されているポインタを実際にコピーしません。代わりに、ポインタを転送し、以前のstd::auto_ptrオブジェクトを空にします。これは、厳密な所有権を実装する 1 つの方法であり、std::auto_ptr一度に 1 つのオブジェクトのみがポインタを所有できます。つまり、std::auto_ptrコピー セマンティクスが必要な場所では を使用すべきではありません。[ 15 ]は既にコピー セマンティクスを持って存在していたため、既存のコードとの後方互換性を損なうことなく、ムーブ オンリー ポインタにアップグレードすることはできませんでした。std::auto_ptr
Rust のユニーク ポインタに相当するのは でありstd::boxed::Box、これはヒープに割り当てられたオブジェクトの一意の所有権を持ちます。[ 16 ]std::ptr::Unique Rust の以前のバージョンでは、生の非 null をラップするもありました*mut T。[ 17 ]
の 2 番目のテンプレート パラメータを使用して「スマートvoid ポインタ」を使用することが可能です。このパラメータには、デアロケータのタイプ (デフォルトでは) が格納されます。std::unique_ptrDelstd::default_delete<T>
std :: unique_ptrを使用します。struct MyDeleter { void operator ()( void * p ) const { delete static_cast < int *> ( p ); } };int main () { unique_ptr < void , MyDeleter > myPointer ( new int ( 42 ), MyDeleter ()); }C++11 では、ヘッダーファイルで定義されているstd::shared_ptrと が導入されました。[ 14 ] C++11 では、RAIIパラダイムで動的メモリを安全に割り当てるために(は C++14 で導入されました)も導入されました。[ 18 ]std::weak_ptr<memory>std::make_sharedstd::make_unique
Aは生ポインタstd::shared_ptrを格納するコンテナです。A は、その格納されているポインタの参照カウント所有権を、A のすべてのコピーと連携して維持します。格納されている生ポインタによって参照されるオブジェクトは、A のすべてのコピーが破棄されたときにのみ破棄されます。std::shared_ptrstd::shared_ptr
import std ;std :: shared_ptrを使用します。shared_ptr < int > p0 ( new int ( 5 )); // 有効。1 つの整数を割り当て、値 5 で初期化します。shared_ptr < int [] > p1 ( new int [ 5 ]); // 有効。5 つの整数を割り当てます。shared_ptr < int [] > p2 = p1 ; // 両方ともメモリを所有します。p1.reset (); // p2 によりメモリはまだ存在します。p2.reset (); //他にメモリを所有する人がいないため、メモリが解放されます。Astd::weak_ptrは生ポインタを格納するコンテナです。これは のコピーとして作成されます。のコピーshared_ptrの存在や破棄は、やその他のコピーには影響を与えません。 のすべてのコピーが破棄されると、すべてのコピーは空になります。std::weak_ptrstd::shared_ptrstd::shared_ptrstd::shared_ptrstd::weak_ptr
import std ;using std :: shared_ptr ; using std :: weak_ptr ;shared_ptr < int > p1 = std :: make_shared < int > ( 5 ); weak_ptr < int > wp1 { p1 }; // p1 がメモリを所有します。{ shared_ptr < int > p2 = wp1 . lock (); // これで p1 と p2 がメモリを所有します。// p2 は弱いポインタから初期化されるため、メモリがまだ存在するかどうかを確認する必要があります。if ( p2 ) { useSomePointer ( p2 ); } } // p2 は破棄されます。メモリは p1 が所有します。p1.reset ( ); // メモリを解放します。shared_ptr <int> p3 = wp1.lock ( ) ; // メモリがなくなったので、空の shared_ptr を取得します。if ( p3 ) { //コードは実行されませんuseLivePointer ( p3 ); }の実装では参照カウントをstd::shared_ptr使用するため、循環参照が問題となる可能性があります。循環参照チェーンは、参照の 1 つを に変更することで解消できます。std::shared_ptrstd::weak_ptr
複数のスレッドは、同じオブジェクトを指す異なるオブジェクトにstd::shared_ptr同時に安全にアクセスできます。 [ 19 ]std::weak_ptr
参照対象物は、ねじの安全性を確保するために別途保護する必要があります。
std::shared_ptrこれらはBoostライブラリstd::weak_ptrで使用されているバージョンに基づいています。C ++テクニカルレポート1 (TR1)で汎用ユーティリティとして初めて標準に導入されましたが、C++11ではBoostバージョンに合わせてさらに多くの機能が追加されました。
Javaでは、すべてのオブジェクトは実質的に共有参照であり、Java仮想マシンstd::rc::Rc(JVM)がオブジェクトへの到達可能性を追跡します。Rustには、 (参照カウント、シングルスレッド)とstd::sync::Arc(アトミック参照カウント、スレッドセーフ)の2種類の共有ポインタがあります。
Java には弱ポインタと同等の概念java.lang.ref.WeakReference(弱参照を実装) があり、オブジェクトの参照カウントを増やさず、を使用してオブジェクトがまだ生きているかどうかを確認できますget()。同様に Java には(ファントム参照java.lang.ref.PhantomReferenceを実装) があり、オブジェクトがガベージ コレクションされた後、ファイナライズされる前にクリーンアップ アクションを実行するためにとともに使用されます。 とは異なり、 は実際にオブジェクトにアクセスできません。一方、 はガベージ コレクタの裁量に従ってクリアされます。Rust には、またはを使用して作成される、弱く所有権を持たない参照のための があります。[ 20 ]java.lang.ref.ReferenceQueuejava.lang.ref.WeakReferencejava.lang.ref.PhantomReferencejava.lang.ref.SoftReferencestd::rc::Weakstd::rc::Rcstd::sync::Arcdowngrade()
一般的な C++ ライブラリやカスタムSTLに実装されている他のタイプのスマート ポインタ (C++ 標準には含まれていない) もあり、例としては侵入型ポインタなどがあります。[ 21 ] [ 22 ]