「C++の基礎 - スマートポインタ(unique ptr)」の版間の差分

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動
201行目: 201行目:
これは、オブジェクトの生成・破棄に比べれば遥かに軽い操作であるが、スレッドセーフにするために排他されていたりもするので0ではない。<br>
これは、オブジェクトの生成・破棄に比べれば遥かに軽い操作であるが、スレッドセーフにするために排他されていたりもするので0ではない。<br>
<br><br>
<br><br>
== std::vectorクラスとの比較 ==
<code>std::vector</code>クラスと<code>std::unique_ptr</code>クラスにおいて、両方のアプローチにはそれぞれ利点がある。<br>
<br>
* std::vectorを使用する方法
*: サイズの動的な変更が容易である。
*: STLアルゴリズムとの互換性が高い。
*: メモリ割り当てと解放が自動的に行われる。
*: <br>
* std::unique_ptrを使用する方法
*: 生の配列に近い性能を維持しつつ、安全なメモリ管理が可能である。
*: サイズが固定の場合に適している。
<br>
どちらの方法も、以前の実装と比べてメモリリークのリスクが低く、より現代的なC++のスタイルに則っている。<br>
プロジェクトの要件や好みに応じて、適切な方法を選択すること。<br>
<br>
<code>std::vector</code>クラスを使用する方法は、より柔軟性が高く、一般的に推奨される。<br>
一方で、<code>std::unique_ptr</code>クラスを使用する方法は、パフォーマンスが重要で、配列サイズが固定の場合に適している。<br>
<br><br>


__FORCETOC__
__FORCETOC__
[[カテゴリ:C++]]
[[カテゴリ:C++]]

2024年7月28日 (日) 20:45時点における版

概要

C++11では、unique_ptr<T>、shared_ptr<T>、weak_ptr<T>の3種のスマートポインタが追加された。
これらのスマートポインタは、メモリの動的確保の利用の際に生じる多くの危険性を低減する目的で使用されるが、
それぞれ独自の考え方と機能を持っている。

3種のスマートポインタを適切に使い分けることで、安全性と開発速度の向上が見込めるだけでなく、
プログラマの意図に合わせてポインタを記述し分けることができる、非常に強力なツールとなる。

ここでは、スマートポインタについて初めて学ぶ人を対象に、
C++11で追加された3種のスマートポインタの機能と使い方、および3種をどのように考えて使うかについて、初歩的な解説を行う。


unique_ptrとは

unique_ptr<T>は、あるメモリに対する所有権を持つポインタが、ただ一つであることを保証するようなスマートポインタである。
テンプレート引数で保持するポインタ型を指定し、スマートポインタが破棄される際にディストラクタにおいて自動的にメモリを開放する。

unique_ptr<T>は、以下の様な特徴を持っている。
・あるメモリの所有権を持つ unique_ptr<T>は、 ただ一つのみである。
・コピーが出来ない。代わりに、C++11で新たに追加されたムーブによって、所有権を移動することができる。
・通常のポインタに匹敵する処理速度。
・配列を扱うことができる。
・deleter(後述)を指定することができる。

 #include<iostream>
 #include<memory>    // スマートポインタを使用する時に指定する
 
 class hoge
 {
    private:
       std::unique_ptr<int> ptr;
 
    public:
       hoge(int val_) : ptr(new int(val_)){}
       int getValue()const{return *ptr;}
 };
 
 int main()
 {
    // hogeのコンストラクタでint型を動的に確保しunique_ptrに委ねる
    hoge Hoge(10);
 
    // コンストラクタの引数として、動的確保したメモリのアドレスを指定
    std::unique_ptr<int> ptr(new int(10));
 
    // reset関数を使用して後から代入できる
    std::unique_ptr<int> ptr2;
    ptr2.reset(new int(10));
 
    // C++14以降では、make_unique関数が使用できる
    std::unique_ptr<int> ptr3 = std::make_unique<int>(10);
 
    // unique_ptrはコピーコンストラクタで作成しようとすると、コンパイルエラーになる
    //hoge Hoge2(Hoge);
 
   // 明示的にmoveするならOK
   hoge Hoge2(std::move(Hoge));
 
   return 0;
 }



unique_ptrの使い方

詳しい使い方を下記に示す。
尚、使用の際には#include <memory>を指定する必要がある。

 // まず、unique_ptr<T>にメモリの所有権を委ねるには、コンストラクタで指定するかreset(pointer)を使う。
 // C++14以降では、make_unique<T>関数を使って作成することができる。
 
 // コンストラクタの引数として、動的確保したメモリのアドレスを指定
 std::unique_ptr<int> ptr(new int(10));
 
 // reset関数を使って、後から代入することもできる
 std::unique_ptr<int> ptr2;
 ptr2.reset(new int(10));
 
 // C++14以降であれば、make_unique関数を使うこともできる
 std::unique_ptr<int> ptr3=std::make_unique<int>(10);


 // unique_ptr<T>は、コピーは禁止されているが、move関数は使用することができる。
 std::unique_ptr<int> ptr(new int(10));
 
 // コピーコンストラクタや、コピー代入演算子はエラー
 //std::unique_ptr<int> ptr2(ptr); //===ERROR===
 //std::unique_ptr<int> ptr2;
 //ptr2 = ptr;
 
 // ムーブコンストラクタや、ムーブ代入演算子はOK
 // この時、所有権が移動する
 std::unique_ptr<int> ptr3(std::move(ptr)); // ptrの所有権がptr3に移動する
 
 std::unique_ptr<int> ptr4;
 ptr4 = std::move(ptr3);                   // ptr4の所有権がptr5に移動する


 // メモリの解放は、ディストラクタや reset(pointer)を使う。
 
 // 引数なしやnullptrを引数としてreset関数を呼んでも、明示的に解放できる
 std::unique_ptr<int> ptr(new int(10));
 ptr.reset();


 // 生のポインタが欲しい時は、get()かrelease()を使う
 
 // get()は、生ポインタを得るだけで、ポインタの所有権はunique_ptr<T>が保持し続ける
 int *pint;
 pint = ptr.get();
 
 // release()は、ポインタの所有権自体も放棄するため、メモリの開放は自分で行う必要がある
 pint = ptr.release();
 delete pint;


 // unique_ptr<T[]>のように指定すれば、配列を扱うこともできる
 // 配列型の場合、operator[](size_t)を使用することができる
 // int型配列の要素数10を宣言
 std::unique_ptr<int[]> ptrArray(new int[10]);
 // または
 std::unique_ptr<int[]> ptrArray = std::make_unique<int[]>(10);
 
 for(int i = 0; i < 10; i++)
 {
    ptrArray[i] = i;
 }


 // ポインタの保持するメモリにアクセスするには、通常のポインタ同様に、operator*()やoperator->()が使用できる
 std::unique_ptr<std::string> pStr(new std::string("test"));
 
 // operator*()でstring型を参照(testと表示される)
 std::cout << *pStr << std::endl;        
 
 // operator->()で、string型のsize関数を呼び出せる
 unsigned int StrSize = pStr->size();



スマートポインタの使いどころ

関数の引数

関数を通して所有権を渡す時は、std::unique_ptrまたはstd::shared_ptrを使用する。
std::unique_ptrは完全に所有権を譲渡、std::shared_ptrは共有する。
それ以外は、生ポインタ(または参照)を使用する。

関数の戻り値

ファクトリ関数(オブジェクトを生成する関数)の戻り値は、原則、std::unique_ptrを使用する。

std::shared_ptrで扱いたい場合、受け取ったstd::unique_ptrstd::shared_ptrに代入するとよい。
(std::shared_ptrのコンストラクタにおいて、std::unique_ptr&&を取るものが存在する)

 // factory methodで作成したオブジェクト
 std::unique_ptr<Foo> CreateFoo();
 
 // std::unique_ptrをstd::shared_ptrに代入する
 std::shared_ptr<foo> sp = CreateFoo();


オブジェクト内のメンバへのポインタを返す場合等は、生ポインタまたは参照でよい。
変更が不要な場合、constキーワードを付加すること。
また、オブジェクトの生存期間には注意が必要である。

 class Foo
 {
    private:
       Bar bar;
 
    public:
       const Bar& GetBar() const { return bar; }
 };


クラスのメンバ変数

所有権を持っている場合、std::unique_ptrまたはstd::shared_ptrを使用する。
所有権を持っていない場合、生ポインタまたは参照を使用する。
この時、参照しているオブジェクトの生存期間に注意が必要である。

生存期間の制御が難しい場合、std::weak_ptrを使用することもある。

std::shared_ptrstd::weak_ptrは必要な場合のみ使用する。
std::shared_ptrを使用することで、オブジェクトの生存期間の制御が難しくなる。

同様に、std::weak_ptrも無闇に使用しない。
複数の場所で使用されるオブジェクトの生存期間を制御することが難しい場合や、循環参照の解決には便利である。
また、所有権を持たないオブジェクトへの参照は全てstd::weak_ptrにする、といったことは行わないこと。

スマートポインタのコスト

std::unique_ptrは、オーバーヘッドはほぼ無い。
メモリ使用量、コードサイズ、実行速度の全てにおいて、生ポインタをnewおよびdeleteする場合に負けることはない。

std::shared_ptrおよびstd::weak_ptrは、オーバーヘッドがある。
コントロールブロックと呼ばれる別のメモリ領域がヒープから取られることで、格納するオブジェクトと合わせて2回のメモリ確保が行われる。
また、コピー時には、リファレンスカウントの操作が行われる。
これは、オブジェクトの生成・破棄に比べれば遥かに軽い操作であるが、スレッドセーフにするために排他されていたりもするので0ではない。


std::vectorクラスとの比較

std::vectorクラスとstd::unique_ptrクラスにおいて、両方のアプローチにはそれぞれ利点がある。

  • std::vectorを使用する方法
    サイズの動的な変更が容易である。
    STLアルゴリズムとの互換性が高い。
    メモリ割り当てと解放が自動的に行われる。

  • std::unique_ptrを使用する方法
    生の配列に近い性能を維持しつつ、安全なメモリ管理が可能である。
    サイズが固定の場合に適している。


どちらの方法も、以前の実装と比べてメモリリークのリスクが低く、より現代的なC++のスタイルに則っている。
プロジェクトの要件や好みに応じて、適切な方法を選択すること。

std::vectorクラスを使用する方法は、より柔軟性が高く、一般的に推奨される。
一方で、std::unique_ptrクラスを使用する方法は、パフォーマンスが重要で、配列サイズが固定の場合に適している。