Geekになりたいママエンジニアのブログ

子育てを楽しみつつ空き時間に自己啓発をしているログです。投稿する内容はすべて個人の意見であり、所属する組織とは関係がありません。

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

C++では、メモリを動的に確保・解放することができます。しかしそれが困難のもとでもあり、動的に確保されたメモリが正しく解放されないと、メモリリークが発生してしまいます。メモリリークが蓄積するとアプリケーションの動作が遅くなったり、パソコン全体の動作が遅くなったりしてしまうことがあります。

なお、Visual Studioを利用している場合、デバッガを利用してメモリリークを検出する方法があります: CRT ライブラリを使用したメモリ リークの検出 - Visual Studio | Microsoft Docs

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

int main()
{
    auto leak = new int[3];
    // delete[] leak;

    _CrtDumpMemoryLeaks();
}

デバッガの出力:

Detected memory leaks!
Dumping objects ->
{76} normal block at 0x00A14F98, 12 bytes long.
 Data: <            > CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

スマートポインター

C++11にて、スマートポインターであるstd::unique_ptr, std::shared_ptr, std::weak_ptrが追加されました。スマートポインターとは、動的に確保されたメモリを適切に管理してくれるような仕組みです。

std::unique_ptr

あるメモリに対して、1つのポインタのみが所有権を保持しているようなポインタです。ポインタが使用されなくなると、自動的にメモリが解放されます。生のポインタとほぼ同様のメモリ効率と処理速度を持ちます。

{
    // c++14以降であればstd::make_uniqueにてポインタの作成が可能。
    auto p1 = std::make_unique<int>(); 

    // コピーすることはできない。
    // auto p2 = p1; // コンパイルエラー。

    // moveセマンティクスによって所有権を移行させることができる。
    std::unique_ptr<int> p3 = std::move(p1);
} // scopeを出たところでメモリが解放される。

メモリ解放の際に使用されるdeleterを指定することができ、deleterによっては生ポインタと比べてメモリ効率などが落ちてしまいます。

std::shared_ptr

あるメモリに対して、複数のポインタが所有権をシェアしているようなポインタです。所有権を保持しているポインタがなくなったタイミングでメモリが解放されます。あるオブジェクトを複数のコードから参照したい場合に便利です。「コントロールブロック」と呼ばれる参照カウンタなどのデータを保持しているため、メモリ使用量などは生のポインタに比べて増えます。

void func(std::shared_ptr<int> ptr)
{
    std::cout << ptr.use_count() << std::endl;
}

int main()
{
    auto p1 = std::make_shared<int>(3);

    // 参照カウンタ1
    std::cout << p1.use_count() << std::endl;

    {
        auto p2 = p1;
        // 参照カウンタ2 (p1とp2)
        std::cout << p1.use_count() << std::endl; 
    }

    // 参照カウンタ1 (p2が解放されp1のみ)
    std::cout << p1.use_count() << std::endl; 

    // 関数内で参照カウンタ2 (引数としてコピーされ参照カウンタが増える)
    func(p1);  

    // 参照カウンタ1 (p1のみ)
    std::cout << p1.use_count() << std::endl; 
}

生のポインタを取り出すときにはget()を使います。この場合p1の所有権に変化はありません。

auto rawp = p1.get();
std::cout << *rawp << std::endl;

unique_ptrは簡単にshared_ptrに変換することができます。メモリ効率の良いunique_ptrで定義しておき、所有権のシェアが必要になったタイミングでshared_ptrに変換する、または関数の戻り値をunique_ptrにしておき、受取先で必要に応じてshared_ptrに変換するようなこともできます。

auto up = std::make_unique<int>(5);
std::shared_ptr<int> sp = std::move(up);

unique_ptrと同様メモリ解放の際に使用されるdeleterを指定することができます。

std::weak_ptr

shared_ptrによって参照されているあるメモリを、所有権を持つことなく参照することのできるポインタです。対象となるメモリが解放されるのをブロックしたくはないけれども、まだ解放されていないのであれば参照したい、といった使い方ができます。循環参照などでメモリが解放されずにリークしてしまう問題を防ぐことができます。shared_ptrと同様「コントロールブロック」と呼ばれるデータを保持します。

参照する際には、lock()を呼び出すことによってshared_ptrとして取り出します。もし既に対象メモリが解放されていたら、lock()はnullポインタを返します。

auto sp = std::make_shared<int>(3);

std::weak_ptr<int> wp(sp);
// 参照カウンタ1 (spのみ, wpは参照カウンタをアップデートしない。)
std::cout << sp.use_count() << std::endl; 

// sp.reset();

if (std::shared_ptr<int> p2 = wp.lock())
{
    // 参照先が利用可能な時のみ処理を行う。
    // 参照カウンタ2 (p1とp2)
    std::cout << sp.use_count() << std::endl; 
    std::cout << "p2 = " << *p2 << std::endl;
}

// 参照カウンタ1 (p1のみ)
std::cout << sp.use_count() << std::endl; 

イメージ

C++ smart pointers
C++ smart pointers