はじめに
C言語やC++言語では、ポインタを使ってメモリの内容を書き換えたり、同じメモリを参照したりします。
メモリリークや解放済みのメモリにアクセスしてアプリがクラッシュする、メモリの2重解放、
などの問題に悩まされたこともあるのではないでしょうか。
本記事では、これらの悩みを解決するスマートポインタについて解説します。
スマートポインタとは?
スマートポインタは、メモリを自動的に解放してくれるC++の機能です。
普通の生ポインタ(Raw Pointer)と違って、スマートポインタを使用すると、
メモリリークや二重解放のエラーを回避できます。
例えば、メモリの動的確保を行うオペレータとして new / delete を使用します。
しかし、new で確保したメモリを delete し忘れると、メモリリークが簡単に発生します。
1 2 3 4 5 6 7 8 |
void func() { int* raw_ptr = new int(10); // 処理... delete raw_ptr; // うっかり忘れるとメモリリーク } |
このようなメモリの解放忘れや、2重解放などをなくすために、
確保したメモリを自動的に解放してくれるテンプレートクラスが考え出されました。
このようなクラスを「スマートポインタ」と呼び、C++11 から標準ライブラリで提供されています。
例示した int* のような生ポインタではなく、スマートポインタを介してメモリを扱うことで、
動的確保の利用の際に生じるメモリの解放忘れ、等の危険性を低減することができます。
C++11 では、unique_ptr<T> 、shared_ptr<T> 、weak_ptr<T> の3種類が用意されており、
それぞれの特徴、使い方を順番に解説します。
unique_ptr とは?
unique_ptr<T> は、1つの所有者しか持てない独占所有権を持ったスマートポインタです。
このため、スマートポインタのデストラクタ内で自動的に delete が呼ばれるため、
メモリリークを防止することができます。
また、ムーブ・セマンティクスをサポートしており、所有権を移動することができます。
特徴
- 動的確保したメモリの所有権を持つ unique_ptr<T> は、ただ一つのみ
- コピーはできないが、ムーブを使用して所有権を移動することができる
- Raw Pointer に劣らない処理速度
- 配列を扱うことができる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <memory> void func() { std::unique_ptr<int> ptr1(new int(10)); // コピーコンストラクタや代入演算子はエラー //std::unique_ptr<int> ptr2(ptr1); // コンパイルERROR //std::unique_ptr<int> ptr3 = ptr1; // コンパイルERROR // ムーブを利用して所有権を移動する std::unique_ptr<int> ptr4(std::move(ptr1)); std::unique_ptr<int> ptr5 = std::move(ptr4); } // ptr5 が所有しているメモリが解放される |
shared_ptr とは?
shared_ptr<T> は、複数の所有者を持つことができる共有所有権を持ったスマートポインタです。
このため、複数の所有者が存在する間はメモリが解放されず、所有者がいなくなった時に
自動的に delete が呼び出されます。
特徴
- メモリの所有権を複数の shared_ptr<T> で共有できる
- コピーもムーブも可能
- 内部で参照カウンタを備えており、若干処理速度は劣る
- 配列を扱うことができるが、明示的に deleter を指定する必要がある
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <memory> void func() { std::shared_ptr<int> x = std::make_shared<int>(10); // new int(10) の代わり // 参照カウンタは現在 1 { std::shared_ptr<int> y = x; // コピーコンストラクタで所有権を共有 // 参照カウンタは現在 2 処理... } // y が破棄され、参照カウンタが 1 となる 処理... } // x が破棄されるタイミングで参照カウンタが 0 となり、自動的に delete される |
shared_ptr<T> は、所有権を持つポインタの数を記録するカウンタを内部に持っています。
このカウンタのことを「参照カウンタ」や「リファレンスカウンタ」と呼びます。
所有権を持つ shared_ptr<T> がコピーされると、内部でカウンタがインクリメントされ、
ディストラクタや明示的な解放時にデクリメントされます。
全ての所有者がいなくなる、つまりカウンタがゼロになると、
不必要なメモリとして自動的に delete が呼び出され、メモリが解放されます。
カウンタで所有者数を管理することで、複数の shared_ptr<T> が所有権を保持していても、
適切なタイミングで一度だけメモリ解放が実行される仕組みです。
weak_ptr とは?
weak_ptr<T> は、循環参照によって生じる問題を防ぐために導入されたスマートポインタです。
普段は使うことはあまりありませんが、shared_ptr<T> を使って循環参照する場合に、
shared_ptr<T> と組み合わせて使うことができます。
循環参照とは、以下のような場合を指します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include <memory> class Hoge { public: std::shared_ptr<Hoge> ptr; }; void func() { std::shared_ptr<Hoge> ph1 = std::make_shared<Hoge>(); std::shared_ptr<Hoge> ph2 = std::make_shared<Hoge>(); ph1->ptr = ph2; ph2->ptr = ph1; } // shared_ptr のデストラクタが呼ばれても、確保した2つのHogeは解放されない |
Hoge のインスタンスは 2 つしか生成していませんが、相互に参照することにより、
実際の個数よりも多くカウントされてしまい、カウンタがゼロにならないため問題が起きます。
この問題を解決するために weak_ptr<T> を使用します。
1 2 3 4 5 6 7 |
#include <memory> class Hoge { public: std::weak_ptr<Hoge> ptr; }; |
Hoge クラス内のメンバ変数を weak_ptr<T> に変更するだけで、
先ほどの問題は起きなくなります。
make_shared と make_unique
サンプルコードの中でシレっと使いましたが、shared_ptr<T> と unique_ptr<T> の
スマートポインタを生成するユーティリティ関数が用意されています。
make_shared<T> と make_unique<T> です。
メモリアロケーションの回数を減らすために使用され、1つの関数でオブジェクトの生成と
スマートポインタの生成を同時に行うことができます。
スマートポインタの注意点
スマートポインタは、C++言語の仕様(コンストラクタ・デストラクタ)をうまく利用した仕組みで、
動的メモリ確保、解放を行う際に安全にポインタを操作できるようになるでしょう。
ただし、以下の点に注意する必要があります。
注意ポイント
- Raw Pointer(従来の生ポインタ)への変換は避けること
- 循環参照に注意すること
- 処理性能に関する注意点があること
これらの注意点を守らなければ、従来の生ポインタの時と同じように、
メモリリークや二重解放の問題を起こしてしまいます。