unique_ptr | Programming Place Plus Modern C++編【標準ライブラリ】 第3章

トップページModern C++編

Modern C++編は作りかけで、更新が停止しています。代わりに、C++14 をベースにして、その他の方針についても見直しを行った、新C++編を作成しています。
Modern C++編は削除される予定です。

この章の概要 🔗

この章の概要です。


概要 🔗

std::unique_ptrスマートポインタの一種で、確保されたリソースを指すポインタを占有管理します。スマートポインタは、【言語解説】第15章で取り上げていますが、リソースの管理を自動化するなどのスマート(賢い)な機能を持ち、ポインタとしての機能を備えたクラス(通常はクラステンプレート)です。

std::unique_ptr はクラステンプレートです。デストラクタで、リソースの解放を行うようになっているため、解放に関する処理を std::unique_ptr に一任しておけば、解放忘れや二重解放を防げます。

std::unique_ptr は、<memory> という標準ヘッダで、以下のように定義されています。

namespace std {
    template <typename T, typename D = std::default_delete<T>>
    class unique_ptr;

    template <typename T, typename D>
    class unique_ptr<T[], D>;
}

テンプレート仮引数 T が、管理するポインタが指す型です。テンプレート仮引数 D はデリータの指定です。デリータについては、「デリータ」の項で取り上げます。

2つある std::unique_ptr の定義のうち、1つ目のものは、管理対象のポインタが単独のリソースを指している場合に使うものです。2つ目のものは、リソースが配列になっている場合に使います。もっともありふれた例でいえば、1つ目の方は new で得たポインタを管理し、2つ目の方は new[] で得られたポインタを管理します。


初期化と破棄 🔗

std::unique_ptr のコンストラクタに、生のポインタを渡すと、そのポインタが指しているリソースの解放を std::unique_ptr に任せることになります。言い換えると、std::unique_ptr がリソースの管理権限を持ちます。このような権限を、所有権という言葉で表現します。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

int main()
{
    std::unique_ptr<MyClass> p(new MyClass());
}

実行結果:

Constructor
Destructor

スマートポインタを使うと決めたなら(通常そうするべきです)、一瞬たりとも、スマートポインタに所有されていない瞬間を作らないようにしなければなりません。つまり、このサンプルプログラムのように、コンストラクタの実引数のところで生成を行うのです。次のような書き方をすると、瞬間的にはスマートポインタが所有していないタイミングができてしまうため、好ましくありません。

#include <memory>

int main()
{
    int* n = new int(100);
    std::unique_ptr<int> p(n);
}

実行結果:

たとえば、n を得て、スマートポインタに引き渡すまでの間に他の処理がある(または追加される)と、解放忘れにつながる恐れがあります。また何より、例外(【言語解説】第18章)発生時に、確実に解放忘れにつながってしまいます。

std::unique_ptr のデフォルトコンストラクタは、何も管理していない状態(所有権を持たない状態)で初期化します。ヌルポインタを渡した場合も同様の状態になります。

std::unique_ptr にはコピーコンストラクタがありません。std::unique_ptr は、リソースを占有して管理するものなので、コピーができると都合が悪いためです。一方で、ムーブコンストラクタは持っています。こちらは後で解説します

std::unique_ptr のデストラクタでは、デフォルトでは delete を使った解放を行います。この動作は、デリータの機能を使って変更できます。

ポインタを取得する 🔗

管理している生のポインタは、getメンバ関数を使って取得できます。この関数は、ポインタを管理していないときには nullptr を返します。

#include <iostream>
#include <memory>

int main()
{
    std::unique_ptr<int> p1(new int(100));

    int* rawPtr = p1.get();
    std::cout << *rawPtr << std::endl;

    std::unique_ptr<int> p2;
    rawPtr = p2.get();
    if (rawPtr == nullptr) {
        std::cout << "null" << std::endl;
    }
    else {
        std::cout << "not null" << std::endl;
    }
}

実行結果:

100
null

ポインタ操作 🔗

配列版でない std::unique_ptr は、生のポインタと同じように、*演算子や ->演算子を使えます。

#include <iostream>
#include <memory>

class MyClass {
public:
    explicit MyClass(int n) : mNum(n)
    {}

    inline void SetNum(int n)
    {
        mNum = n;
    }
    inline int GetNum() const
    {
        return mNum;
    }

private:
    int mNum;
};

int main()
{
    std::unique_ptr<MyClass> p(new MyClass(100));

    const MyClass& c = *p;
    p->SetNum(200);

    std::cout << p->GetNum() << std::endl;
    std::cout << c.GetNum() << std::endl;
}

実行結果:

200
200

*演算子は、管理しているポインタが指す先にあるものを左辺値参照で返します。いわゆるポインタの間接参照を実現します。->演算子は、管理しているポインタを通して、指す先にあるものを操作するときに使います。

当然、ポインタを管理していないときにこれらの操作を行うと、未定義の動作となってしまうので注意してください。ポインタを管理しているかどうかを調べるには、getメンバ関数が nullptr を返さないことを確認するか、bool型への型変換演算子(【言語解説】第9章)を利用して、以下のように問い合わせます。

// std::unique_ptr の p がポインタを管理しているか?
if (p) {}

ポインタを再設定する 🔗

resetメンバ関数を使うと、管理対象のポインタを変更できます。もともと何らかのポインタを管理していた場合は、先に解放処理が行われます。

また、実引数がない resetメンバ関数を呼ぶか、実引数に nullptr を指定すると、何も管理していない状態にできます。こちらも、もともと何らかのポインタを管理していた場合は、解放処理が行われます。

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

int main()
{
    std::unique_ptr<MyClass> p(new MyClass());

    p.reset(new MyClass());

    p.reset();
}

実行結果:

Constructor
Constructor
Destructor
Destructor

所有権の変更 🔗

std::unique_ptr は、ムーブコンストラクタとムーブ代入演算子を持っており、所有権を移動できます。所有権を移動させるとは、移動元が管理していたポインタは nullptr に置き換えられ、移動先の std::unique_ptr に管理が移るということです。

#include <cassert>
#include <iostream>
#include <memory>
#include <utility>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

int main()
{
    std::unique_ptr<MyClass> p1(new MyClass());

    std::unique_ptr<MyClass> p2 = std::move(p1);
    assert(!p1 && p2);

    p1 = std::move(p2);
    assert(p1 && !p2);
}

実行結果:

Constructor
Destructor

また、releaseメンバ関数を使うと、生のポインタを戻り値で返し、自身は所有権を手放します。言い換えると、std::unique_ptr から所有権を取り戻す関数ですが、当然、自動的な解放が行われなくなってしまうので、必ず戻り値を受け取って、解放まで面倒を見るようにしなければなりません。

#include <cassert>
#include <iostream>
#include <memory>
#include <utility>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

int main()
{
    std::unique_ptr<MyClass> p(new MyClass());

    MyClass* c = p.release();  // 所有権を失う

    delete c;  // 自力で解放
}

実行結果:

Constructor
Destructor

デリータ 🔗

std::unique_ptr が行う解放処理は、メンバとして保持されているオブジェクトが行います。このオブジェクトは、関数として呼び出せるものでなければなりません(関数、関数オブジェクト)。なお、このオブジェクトのことを、デリータと呼びます。

std::unique_ptr の2つ目のテンプレート仮引数には、デリータの型が指定されます。特に指定しなければ、std::default_delete という標準のデリータが指定されます。これは、非配列のときは delete を、配列のときは delete[] を使って解放を行うように実装されています。

標準でないデリータを使う場合、その方法にはいくつか選択肢があります。std::fopen関数で得たファイルポインタを管理する例を取り上げます。

関数を使う

解放処理を記述した関数を定義して、その関数ポインタを std::unique_ptr のコンストラクタへ渡します。2つ目のテンプレート実引数には、関数の型を指定します。

#include <cstdio>
#include <memory>

int main()
{
    std::unique_ptr<std::FILE, int (*)(std::FILE*)> fp(std::fopen("test.bin", "r"), std::fclose);
}

実行結果:

テンプレート実引数に関数の型を指定する部分は少々面倒な場合もあります。decltype(【言語解説】第20章)を使うのが良いでしょう。

std::unique_ptr<std::FILE, decltype(&std::close)> fp(std::fopen("test.bin", "r"), std::fclose);

関数オブジェクトを使う

関数オブジェクト(【言語解説】第32章)を使う場合は、operator() で解放処理を行うように実装したクラスを定義します。std::unique_ptr の2つ目のテンプレート実引数に、クラスの型を指定すれば、解放時に operator() を呼び出してくれます。

この方法の場合、std::unique_ptr のコンストラクタに、管理してもらうポインタ以外のものを渡す必要はありません。

#include <cstdio>
#include <iostream>
#include <memory>

struct FileCloser {
    void operator()(std::FILE* fp)
    {
        std::cout << "call FileCloser()" << std::endl;
        std::fclose(fp);
    }
};

int main()
{
    std::unique_ptr<std::FILE, FileCloser> fp(std::fopen("test.bin", "r"));
}

実行結果:

call FileCloser()

ラムダ式を使う

関数オブジェクトが使えるのなら、ラムダ式(【言語解説】第32章)を使うことも考えられます。

#include <cstdio>
#include <iostream>
#include <memory>

int main()
{
    auto fileCloser = [](std::FILE* fp) {
        std::cout << "call lambda" << std::endl;
        std::fclose(fp);
    };

    std::unique_ptr<std::FILE, decltype(fileCloser)> fp(std::fopen("test.bin", "r"), fileCloser);
}

実行結果:

call lambda

テンプレート実引数にデリータの型名を指定しなければならないため、いったん、変数にラムダオブジェクトを受け取る形になります。

配列版の unique_ptr 🔗

配列に対応した std::unique_ptr では、1つ目のテンプレート実引数に指定する型を配列型にします。

#include <cassert>
#include <iostream>
#include <memory>
#include <utility>

class MyClass {
public:
    MyClass()
    {
        std::cout << "Constructor" << std::endl;
    }
    ~MyClass()
    {
        std::cout << "Destructor" << std::endl;
    }
};

int main()
{
    std::unique_ptr<MyClass[]> p(new MyClass[5]);

    const MyClass& r = p[3];  // []演算子が適用できる
}

実行結果:

Constructor
Constructor
Constructor
Constructor
Constructor
Destructor
Destructor
Destructor
Destructor
Destructor

配列版の std::unique_ptr では、[]演算子が使用できるようになっています。指定した位置にある要素を指す参照が返されます。範囲外アクセスは未定義の動作です。

実際のところ、要素数を動的に決める配列が必要であれば、std::vector(第6章)や std::basic_string(第10章)を使うことを検討した方が良いです。これらは配列を便利に扱うための機能を備えている上に、適切なメモリ管理も行います。


練習問題 🔗

問題① std::unique_ptr がコピーできないことを確かめてください。また、コピーができない理由を説明してください。

問題② 次のコードを std::unique_ptr を使った形に書き換えてください。Create関数は生成の具体的な処理を隠し、抽象化する目的で存在している関数であり、引き続き使用しなければならないものとします。

class MyClass {};

MyClass* Create()
{
    return new MyClass();
}

int main()
{
    MyClass* p = Create();
}

問題③ 実引数がヌルポインタかどうかを判定して、標準出力へ結果を出力する関数を考えます。生のポインタでもスマートポインタでも使えるように、関数を実装してください。

void PrintNullOrNotNull(/* */)
{
    if (/* */) {
        std::cout << "not null" << std::endl;
    }
    else {
        std::cout << "null" << std::endl;
    }
}


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 新規作成。



前の章へ (第2章 ムーブに関するユーティリティ)

次の章へ (第4章 shared_ptr)

Modern C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る