トラック
/
C++
C++
/
シラバス
/
スマートポインター
スマ

スマートポインター の C++

1個の演習

スマートポインターについて

スマートポインタは、メモリ管理を自動化するために設計されたC++のモダンな機能で、生のポインタにありがちなメモリリークやダングリングポインタの防止に役立ちます。 生のポインタをラップする存在で、ポインタが不要になったときのメモリの自動解放など、追加の機能を備えています。

基本的な構文

スマートポインタは通常、C++標準ライブラリのクラステンプレートとして実装されています。 もっともよく使われるスマートポインタはstd::unique_ptrとstd::shared_ptrの2つです。

ユニークポインタ

std::unique_ptrは、オブジェクトを独占的に所有するスマートポインタです。 ある時点で資源を所有するstd::unique_ptrは1つだけであることが保証されます。 所有しているstd::unique_ptrが破棄されるかリセットされると、そのオブジェクトは自動的に破棄され、メモリが解放されます。

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

std::make_unique()の利点

std::unique_ptrを作るときは、newで直接メモリを確保するよりもstd::make_unique()を使うのが好ましいです。 std::make_unique()にはいくつかの利点があります。

  1. 例外安全性: std::make_unique()は例外安全性を保証します。 オブジェクトの構築中に例外が投げられても、メモリは自動的に解放され、メモリリークを防げます。
  2. 明確さ: std::make_unique()を使うと、コードがより明確で簡潔になります。 テンプレート引数が自動的に推論されるため、確保する型を明示的に指定する必要がありません。
  3. 最適化の余地: コンパイラーは、newで手動でメモリを確保するよりもstd::make_unique()を効果的に最適化できる可能性があり、性能の向上につながることがあります。
  4. 誤用の回避: std::unique_ptrを手動で構築すると、その背後の資源を削除できてしまいます。 その状態でstd::unique_ptrがスコープの終わりに資源を削除しようとすると、未定義動作を引き起こします。

シェアードポインタ

std::shared_ptrは、複数のstd::shared_ptrオブジェクトで同じ資源の所有権を共有できるスマートポインタです。 その資源を参照しているシェアードポインタの数を数えておき、資源を所有する最後のシェアードポインタがスコープを抜けるかリセットされたときにだけ、メモリを解放します。

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

C++17以前では、std::make_shared<T[]>を使って配列を扱うstd::shared_ptrを作ることは直接サポートされていません。 std::make_shared<T[]>で配列を確保することは可能ですが、単一のオブジェクトと配列ではメモリ管理が異なるため、そこから直接シェアードポインタを作ると未定義動作につながることがあります。 代わりに、シェアードポインタで配列をうまく扱うにはstd::vectorや独自の削除関数の使用を検討しましょう。 C++17で配列の確保とシェアードポインタを扱うときは、使用しているコンパイラーと標準ライブラリの実装との互換性を常に確認してください。

std::make_shared()の利点

std::make_unique()と同様に、std::make_shared()にもメモリ効率の向上、例外安全性、可読性といった利点があります。 コントロールブロックと管理対象オブジェクトのメモリ確保を1つの操作にまとめるため、効率が上がり、メモリリークの危険が減ります。 さらに、テンプレート引数が自動的に推論されるので、コードが簡潔になり可読性も高まります。 C++でstd::shared_ptrを扱うときは、std::make_shared()を使うことで、よりクリーンで安全、かつ効率的なコードになります。

ウィークポインタ

std::weak_ptrはstd::shared_ptrの相棒となるクラスで、シェアードポインタが管理するオブジェクトへの、所有権を持たない「弱い」参照を提供します。 資源の寿命に影響を与えずに、その資源へアクセスできます。 ウィークポインタは、メモリリークを防ぐために循環参照を断ち切る必要がある場面で役立ちます。

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptrと循環所有

std::weak_ptrは、std::shared_ptrを使うときに起こりうる循環所有、いわゆる循環参照の問題に対処するために設計されました。

循環所有の状況では、2つ以上のstd::shared_ptrが互いを参照し合い、どのオブジェクトも削除できない循環ができてしまいます。 互いに強い参照を持っているためで、これがメモリリークにつながります。

std::weak_ptrは、参照カウントに加算せずに共有オブジェクトへの弱い参照を持てるようにすることで、この問題を解決します。 つまり、共有オブジェクトを監視してアクセスできますが、その削除を妨げることはありません。 共有オブジェクトへの強い参照がすべて解放されれば、そのオブジェクトは破棄され、それを監視していたウィークポインタは自動的にnullptrにリセットされます。

循環所有を断ち切るためにstd::weak_ptrを使った短い例を次に示します。

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

"Node constructed"が2回出力され、そのあとに"Node destructed"が2行続きます。

nextがウィークポインタではなくstd::shared_ptrだった場合、循環所有の状況が生まれ、両方のノードが互いに強い参照を持ち合って破棄できなくなるため、メモリリークにつながります。

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

この場合、node1とnode2はstd::shared_ptrを通じて互いに強い参照を持ち、循環を作ります。 プログラムが終了し、reset()で強い参照を解放しようとしても、各ノードが互いへの強い参照を持っているため、どちらのノードの参照カウントも0になりません。 そのためノードは破棄されず、メモリリークになります。 "Node constructed"は2回出力されますが、それに続く"Node destructed"の行は1行も出力されません。

ダングリングポインタとstd::weak_ptr

ダングリングポインタは、削除済みのオブジェクトをポインタが参照しているときに起こり、そのポインタを間接参照すると未定義動作になります。 std::weak_ptrは、std::shared_ptrが管理するオブジェクトへの、所有権を持たない弱い参照を提供することで、ダングリングポインタの回避に役立ちます。 std::weak_ptrを使ってダングリングポインタを避ける手順は次のとおりです。

  1. std::weak_ptrを作る: std::shared_ptrを直接持つのではなく、共有オブジェクトへのstd::weak_ptrを作ります。

  2. 有効性を確認する: std::weak_ptrを使う前に、expired()関数で有効性を確認します。 この関数は、対応する共有オブジェクトが削除済みであればtrueを返します。

  3. std::weak_ptrをロックする: 共有オブジェクトに安全にアクセスするにはlock()関数を使います。オブジェクトがまだ有効なら、同じオブジェクトを指すstd::shared_ptrが返ります。 オブジェクトが削除済みなら、lock()は空のstd::shared_ptrを返します。

次のコード例は、ダングリングポインタを避けるためにstd::weak_ptrを使う例です。

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

この例で、node1->nextとnode2->nextはstd::weak_ptrオブジェクトです。 共有オブジェクトにアクセスする前に、expired()関数で有効性を確認します。 次に、lock()を使って共有オブジェクトに安全にアクセスします。 この方法で、std::weak_ptrを扱うときのダングリングポインタを避けられます。

使い分けの指針

スマートポインタを既定で使いましょう。独占的な所有にはstd::unique_ptr、共有の所有にはstd::shared_ptrです。 生のポインタは、所有権を持たない参照や、古いコードとやり取りするときに取っておきます。 ほとんどの場合、独占的な所有にはstd::unique_ptrで十分です。参照カウントの負担なしに、軽量なメモリ管理が得られるからです。 std::shared_ptrは、本当に共有の所有が必要なとき以外は、負担と複雑さを招くので控えめに使いましょう。 std::weak_ptrは循環参照を断ち切ったり共有オブジェクトを監視したりするための専門的なもので、それほど頻繁には使いません。

GitHubで編集 リンクは新しいウィンドウまたはタブで開きます

スマートポインターを学習する