C++

std::shared_ptrとstd::unique_ptrをわかりやすく

はじめに

先日、「newしたポインタのvectorをスコープ外からdeleteする方法」という記事でスマートポインタを使用すると良いとコメントをいただいたため、調べてわかったことと使い方をメモしておきます。

そもそもの問題のコード

以前の記事にも書いたことを再掲させていただきます。

newで作成したポインタをvectorに格納しておいたのですが、newした作成したスコープの外でdeleteをしたい時にどうすればよいかという問題です。

#include<iostream>

// 使用するクラス
class Hoge {
public:
    Hoge(int _hoge);
    void get();
private:
    int hoge;
}

Hoge::Hoge(int _hoge) {
    hoge = _hoge;
}

void Hoge::get() {
    return hoge;
}

// main文
int main(int argc, char* argv[]) {
    // Hogeポインタのvector
    std::vector<Hoge*> vecHoge;

    for (int i = 0; i < 10; ++i) {
        // newでメモリ確保
        Hoge* tmp = new Hoge(i);
        // vectorにpointerを格納
        vecHoge.push_back(tmp);

        //==============================================================//
        // tmpがスコープから外れてしまうので。これ以降のメモリ解放ができない。//
        // しかし、ここでtmpのメモリを解放してしまうと、                    //
        // このあとでvecHogeからインスタンスにアクセスできない。            //
        //==============================================================//
    }

    for (int i = 0; i < vecHoge.size(); ++i) {
        std::cout << vecHoge[i].get() << std::endl;
    } 

    //==========================//
    // ここでメモリの解放をしたい //
    //==========================//

    return 0;
}

std::shared_ptr

説明

std::shared_ptrは対象のポインタへの所有権を共有(share)するスマートポインタです。
対象のポインタを複数人で共有し、それを何人で共有しているか(「参照カウント」と呼ぶ)監視しておき、誰も参照しなくなった時点でメモリ開放を行います。

使い方

まずスマートポインタを使用するためにはmemoryをインクルードする必要があります。
std::shared_ptrに格納したポインタにアクセスするためにはメンバ関数のgetを使用します。
std::shared_ptrの参照カウントを取得するメンバ関数のuse_countを使って参照カウントを実際に監視してみます。

#include<iostream>
#include<vector>

// スマートポインタを使用するためにインクルード
#include<memory>

// 使用するクラス
class Hoge {
public:
    Hoge(int _hoge);
    ~Hoge();
    int get();
private:
    int hoge;
};

Hoge::Hoge(int _hoge) {
    hoge = _hoge;
}

Hoge::~Hoge() {
    std::cout << "destruct Hoge(" << hoge << ")" << std::endl;
}

int Hoge::get() {
    return hoge;
}

int main(int argc, char* argv[]) {
    std::vector<std::shared_ptr<Hoge>> sharedHogePtrVec;

    std::cout << "newしてメモリ確保した時の参照カウント" << std::endl;
    for (int i = 0; i < 5; ++i) {
        // newで作成したポインタのshared_ptrオブジェクトの作成
        std::shared_ptr<Hoge> bufSharedHogePtr(new Hoge(i));

        // vectorに追加
        sharedHogePtrVec.push_back(bufSharedHogePtr);

        // 参照カウントの出力
        std::cout << "Hoge(" << (sharedHogePtrVec[i].get())->get() << ") reference count: " << sharedHogePtrVec[i].use_count() << std::endl;

        //=========================================================//
        // ここの時点でnewで作成したポインタ自体のスコープは外れる //
        //=========================================================//
    }

    std::cout << "vectorの要素の参照カウントの出力" << std::endl;
    for (int i = 0; i < 5; ++i) {
        //============================================//
        // ここではsharedHogePtrVecのみが参照している //
        //============================================//

        // 参照カウントの出力
        std::cout << "Hoge(" << (sharedHogePtrVec[i].get())->get() << ") reference count: " << sharedHogePtrVec[i].use_count() << std::endl;
    }

    //================================================//
    // 以前の記事はここでdeleterを呼び出していたが    //
    // スコープが外れる時に参照カウントが0になるので  //
    // 自動的にdestructorが呼ばれ、メモリが開放される // 
    //================================================//

    std::cout << "このあとdestructorが呼び出される" << std::endl;

    return 0;
}

出力は以下の通り。
最初はnewを作成した直後なので、作成したポインタとそれを格納したvectorの2つから参照されているため、参照カウントは2です。
次のfor文ではvectorからのみ参照されているため、参照カウントは1です。
最後はvectorがスコープから外れるため、destructorが自動で呼ばれています。

newしてメモリ確保した時の参照カウント
Hoge(0) reference count: 2
Hoge(1) reference count: 2
Hoge(2) reference count: 2
Hoge(3) reference count: 2
Hoge(4) reference count: 2
vectorの要素の参照カウントの出力
Hoge(0) reference count: 1
Hoge(1) reference count: 1
Hoge(2) reference count: 1
Hoge(3) reference count: 1
Hoge(4) reference count: 1
このあとdestructorが呼び出される
destruct Hoge(0)
destruct Hoge(1)
destruct Hoge(2)
destruct Hoge(3)
destruct Hoge(4)

std::unique_ptr

説明

std::unique_ptrは所有権を唯一(unique)持っているように振る舞うスマートポインタです。
つまり、複数のstd::unique_ptrのインスタンスが同一のメモリ領域を管理することはありません。
std::shared_ptrと同様に変数の寿命が来た時に、自動でメモリ領域を解放してくれます。
以前のauto_ptrに置き換わるものにあたるようです。

唯一といっているくらいなので、コピー不可能ですが、ムーブによって所有権を他のstd::unique_ptrへ譲渡することが可能です。

使い方

基本的にはstd::shared_ptrと大きく違わないが、vectorにpush_backする際に所有権を譲渡するstd::moveを行う必要があることを注意する。

#include<iostream>
#include<vector>

// スマートポインタを使用するためにインクルード
#include<memory>

// 使用するクラス
class Hoge {
public:
    Hoge(int _hoge);
    ~Hoge();
    int get();
private:
    int hoge;
};

Hoge::Hoge(int _hoge) {
    hoge = _hoge;
}

Hoge::~Hoge() {
    std::cout << "destruct Hoge(" << hoge << ")" << std::endl;
}

int Hoge::get() {
    return hoge;
}

int main(int argc, char* argv[]) {
    std::vector<std::unique_ptr<Hoge>> uniqueHogePtrVec;

    std::cout << "newしてメモリ確保した時のvectorの中身を確認" << std::endl;
    for (int i = 0; i < 5; ++i) {
        // newで作成したポインタのunique_ptrオブジェクトの作成
        std::unique_ptr<Hoge> bufUniqueHogePtr(new Hoge(i));

        // vectorに追加
        //=====================================================//
        // ここの時に所有権をvectorに譲渡する必要がある            //
        //======================================================//
        uniqueHogePtrVec.push_back(std::move(bufUniqueHogePtr));

        // vectorの中身を確認
        std::cout << "Hoge(" << uniqueHogePtrVec[i]->get() << ")" << std::endl;
    }

    std::cout << "vectorの中身を確認" << std::endl;
    for (int i = 0; i < 5; ++i) {
        // vectorの中身を確認
        std::cout << "Hoge(" << uniqueHogePtrVec[i]->get() << ")" << std::endl;
    }

    //=============================================//
    // 以前の記事はここでdeleterを呼び出していたが    //
    // スコープが外れる時に                          //
    // 自動的にdestructorが呼ばれ、メモリが開放される  // 
    //==============================================//

    std::cout << "このあとdestructorが呼び出される" << std::endl;

    return 0;
}

出力は以下の通り。
ちゃんとdestructorが呼ばれていることがわかる。

newしてメモリ確保した時のvectorの中身を確認
Hoge(0)
Hoge(1)
Hoge(2)
Hoge(3)
Hoge(4)
vectorの中身を確認
Hoge(0)
Hoge(1)
Hoge(2)
Hoge(3)
Hoge(4)
このあとdestructorが呼び出される
destruct Hoge(0)
destruct Hoge(1)
destruct Hoge(2)
destruct Hoge(3)
destruct Hoge(4)