C++のスマートポインタについて

AI実装検定のご案内

目次

C++のスマートポインタとは

C++のスマートポインタとは、動的に確保したオブジェクトの寿命や所有権を自動的に管理するための仕組みです。

従来のC++では、newで確保したメモリをdeleteで手動解放する必要がありました。

int* p = new int(10);

// 処理

delete p;

しかし、この方法ではdeleteを書き忘れたり、途中でreturnしたりすると、確保したメモリが解放されないことがあります。

このような状態をメモリリークと呼びます。

スマートポインタを利用すれば、管理対象のオブジェクトが不要になったタイミングで自動的に破棄されるため、手動でdeleteを管理する必要を大きく減らせます。

現代のC++では、動的メモリの所有権を生ポインタで直接管理するよりも、スマートポインタなどのRAIIに基づいた仕組みを利用するのが基本です。

スマートポインタとRAIIの関係

スマートポインタは、C++で重要なRAIIという考え方に基づいています。

RAIIは「Resource Acquisition Is Initialization」の略で、オブジェクトの生成と同時にリソースを取得し、オブジェクトの破棄と同時にリソースを解放する設計手法です。

例えば、次のようにstd::unique_ptrを使用できます。

#include <memory>

void func()
{
    auto p = std::make_unique<int>(10);

    if (*p == 10) {
        return;
    }
}

途中でreturnしても、pがスコープから外れる際に管理対象のintが自動的に破棄されます。

そのため、明示的にdeleteを書く必要がありません。

C++で使われる主なスマートポインタ

C++標準ライブラリの<memory>には、主に3種類のスマートポインタがあります。

スマートポインタ特徴主な用途
std::unique_ptr所有者が基本的に1つ単独所有
std::shared_ptr複数の所有者を持てる共有所有
std::weak_ptr所有権を持たないshared_ptrの監視や循環所有対策

基本的には、まず値として保持できないかを検討します。

動的な所有権管理が必要であればstd::unique_ptrを優先し、複数の場所で本当に寿命を共有する必要がある場合にstd::shared_ptrを利用します。

std::weak_ptrは、shared_ptrで管理されているオブジェクトを所有せず参照したい場合に使用します。

std::unique_ptrとは

std::unique_ptrは、基本的に1つのスマートポインタだけがオブジェクトを所有するための仕組みです。

#include <iostream>
#include <memory>

int main()
{
    auto p = std::make_unique<int>(100);

    std::cout << *p << '\n';
}

main()が終了してpがスコープから外れると、管理しているintも自動的に破棄されます。

make_uniqueで生成する

unique_ptrは次のように生成することもできます。

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

ただし、通常はstd::make_uniqueを使用する方が簡潔です。

auto p = std::make_unique<int>(100);

make_uniqueを使えば、生のnewを直接記述する必要がなくなります。

unique_ptrはコピーできない

unique_ptrはコピーできません。

auto p1 = std::make_unique<int>(100);

auto p2 = p1; // コンパイルエラー

unique_ptrは単独所有を表現する型であるため、同じオブジェクトの所有権を単純に複製することはできません。

std::moveで所有権を移動できる

コピーはできませんが、std::moveを使えば所有権を移動できます。

auto p1 = std::make_unique<int>(100);

auto p2 = std::move(p1);

この処理後、p2がオブジェクトを所有し、p1は空になります。

if (!p1) {
    std::cout << "p1 is empty\n";
}

std::cout << *p2 << '\n';

unique_ptrでは、所有者を増やすのではなく、所有権そのものを移動するのが特徴です。

unique_ptrを関数で扱う方法

unique_ptrを関数で扱う場合は、所有権を移すのか、それとも単にオブジェクトを参照するだけなのかを意識する必要があります。

所有権を渡さない場合

関数がオブジェクトを利用するだけであれば、必ずしもunique_ptrそのものを引数にする必要はありません。

例えば、参照を受け取る方法があります。

void print(const int& value)
{
    std::cout << value << '\n';
}

int main()
{
    auto p = std::make_unique<int>(100);

    print(*p);
}

この場合、print()はオブジェクトの所有権を持ちません。

所有権を関数へ移す場合

関数側に所有権を渡したい場合は、unique_ptrを値として受け取れます。

void func(std::unique_ptr<int> p)
{
    std::cout << *p << '\n';
}

int main()
{
    auto p = std::make_unique<int>(100);

    func(std::move(p));
}

std::moveによって所有権がfunc()へ移動します。

関数呼び出し後、元のpは空になります。

unique_ptrを関数から返す

unique_ptrは関数の戻り値としても利用できます。

std::unique_ptr<User> createUser()
{
    return std::make_unique<User>();
}

使用する側では次のように受け取れます。

auto user = createUser();

unique_ptrはムーブ可能なため、関数からオブジェクトの所有権を返す設計にも適しています。

std::shared_ptrとは

std::shared_ptrは、複数のスマートポインタが1つのオブジェクトを共有して所有するための仕組みです。

#include <iostream>
#include <memory>

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

    auto p2 = p1;

    std::cout << *p1 << '\n';
    std::cout << *p2 << '\n';
}

この場合、p1p2の両方が同じintオブジェクトの共有所有者になります。

shared_ptrは制御ブロックを利用する

shared_ptrは一般に、制御ブロックと呼ばれる管理情報を通じて、オブジェクトの共有所有状態を管理します。

制御ブロックには、共有所有者の数やweak_ptrに関する情報、必要に応じてデリータなどが保持されます。

auto p1 = std::make_shared<int>(100);

std::cout << p1.use_count() << '\n';

auto p2 = p1;

std::cout << p1.use_count() << '\n';

p1だけなら共有所有者数は1です。

p2 = p1によって共有所有者が増えると、通常はuse_count()が2になります。

最後のshared_ptrが所有権を手放すと、管理対象のオブジェクトが破棄されます。

なお、weak_ptrが残っている場合などは、オブジェクトが破棄された後もしばらく制御ブロックが残ることがあります。

make_sharedで生成する

shared_ptrは次のように生成できます。

std::shared_ptr<int> p(new int(100));

ただし、通常はstd::make_sharedを利用します。

auto p = std::make_shared<int>(100);

make_sharedを使うとコードが簡潔になります。

また、一般的な実装ではオブジェクト本体と制御ブロックをまとめて確保できるため、メモリ確保回数を減らせる場合があります。

std::weak_ptrとは

std::weak_ptrは、shared_ptrによって管理されているオブジェクトを、所有権を持たずに参照するためのスマートポインタです。

auto shared = std::make_shared<int>(100);

std::weak_ptr<int> weak = shared;

weakを作成しても、共有所有者数は増えません。

weak_ptrからオブジェクトへアクセスする

weak_ptrはオブジェクトを所有していないため、対象がすでに破棄されている可能性があります。

そのため、次のように直接デリファレンスすることはできません。

// *weak はできない

アクセスするときはlock()を使います。

if (auto p = weak.lock()) {
    std::cout << *p << '\n';
}

対象がまだ存在していれば、lock()は一時的なshared_ptrを返します。

すでに破棄されていれば、空のshared_ptrが返されます。

shared_ptrの循環所有に注意する

shared_ptrで特に注意したいのが循環所有です。

例えば、2つのオブジェクトが互いをshared_ptrで所有すると、共有所有者数が0にならず、オブジェクトが破棄されない可能性があります。

class B;

class A {
public:
    std::shared_ptr<B> b;
};

class B {
public:
    std::shared_ptr<A> a;
};

次のように設定すると、AとBが相互に所有します。

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();

a->b = b;
b->a = a;

外部のabがなくなっても、AがBを所有し、BがAを所有しているため、共有所有関係が残ってしまいます。

weak_ptrで循環所有を断ち切る

片方をweak_ptrにすれば、所有関係の循環を断つことができます。

class B;

class A {
public:
    std::shared_ptr<B> b;
};

class B {
public:
    std::weak_ptr<A> a;
};

weak_ptrは所有権を持たないため、共有所有者数を増やしません。

このように、親子関係や相互参照を持つ設計では、どちらが所有者で、どちらが単なる参照者なのかを明確にすることが重要です。

スマートポインタでクラスを管理する

実際のC++では、整数よりもクラスオブジェクトをスマートポインタで管理するケースが多くあります。

#include <iostream>
#include <memory>

class User
{
public:
    User()
    {
        std::cout << "User created\n";
    }

    ~User()
    {
        std::cout << "User destroyed\n";
    }

    void hello()
    {
        std::cout << "Hello\n";
    }
};

int main()
{
    auto user = std::make_unique<User>();

    user->hello();
}

userがスコープから外れると、Userのデストラクタが自動的に呼び出されます。

明示的にdeleteを書く必要はありません。

get()で生ポインタを取得する

スマートポインタが管理している生ポインタを一時的に取得したい場合は、get()を利用できます。

auto p = std::make_unique<int>(100);

int* raw = p.get();

ただし、rawは所有権を受け取ったわけではありません。

そのため、次のように手動で削除してはいけません。

delete raw; // NG

所有権は引き続きpが持っているため、手動で削除すると二重解放などの未定義動作につながります。

get()で取得したポインタは、基本的に非所有ポインタとして扱います。

unique_ptrのrelease()とreset()

unique_ptrには、所有権や管理対象を変更するための関数があります。

release()で所有権を切り離す

release()を実行すると、管理中の生ポインタを返し、unique_ptr自身はそのオブジェクトを管理しなくなります。

auto p = std::make_unique<int>(100);

int* raw = p.release();

以降、rawが指すオブジェクトは自動的には破棄されません。

別の所有者に引き渡すか、必要に応じて適切な方法で解放する必要があります。

delete raw;

release()は、生ポインタの所有権を受け取る既存APIなどと連携するときに利用されることがあります。

reset()で管理対象を破棄する

reset()を使うと、現在管理しているオブジェクトを破棄して、スマートポインタを空にできます。

auto p = std::make_unique<int>(100);

p.reset();

実行後、pは空になります。

unique_ptrで配列を管理する

unique_ptrは動的配列にも対応しています。

auto p = std::make_unique<int[]>(5);

p[0] = 10;
p[1] = 20;

この場合も、unique_ptrが配列を自動的に破棄します。

ただし、一般的な可変長配列が必要なだけであれば、通常はstd::vectorの方が便利です。

#include <vector>

std::vector<int> values(5);

vectorにはサイズ管理や要素追加、イテレーションなど多くの機能が用意されています。

スマートポインタとnullptr

スマートポインタは、管理対象を持たない空の状態にもできます。

std::unique_ptr<int> p;

この場合、pは空です。

状態を確認するには次のように記述できます。

if (p) {
    std::cout << *p << '\n';
}

または、nullptrと比較できます。

if (p != nullptr) {
    std::cout << *p << '\n';
}

unique_ptrとshared_ptrの使い分け

スマートポインタを利用するときは、単純に「便利そうだからshared_ptrを使う」のではなく、所有権の関係を考える必要があります。

unique_ptrが適しているケース

所有者が1つに決まっている場合は、unique_ptrが適しています。

class Car
{
private:
    std::unique_ptr<Engine> engine;
};

EngineCarが所有する設計であれば、単独所有を表すunique_ptrが自然です。

shared_ptrが適しているケース

複数のオブジェクトが、同じ対象の寿命そのものを共有する必要がある場合はshared_ptrを検討します。

std::shared_ptr<Resource> resource;

ただし、複数の場所からアクセスしたいだけなら、必ずしも共有所有にする必要はありません。

所有者はunique_ptrで1つに保ち、利用側には参照や非所有ポインタを渡す設計も可能です。

shared_ptrをむやみに使わない方がよい理由

shared_ptrを使えば所有権管理が簡単になるように見えますが、すべてをshared_ptrにするのは適切ではありません。

shared_ptrでは制御ブロックによる共有所有状態の管理が必要です。

また、共有所有を増やしすぎると、どのオブジェクトが実質的な所有者なのか分かりにくくなることがあります。

設計するときは、次の順番で検討すると分かりやすいでしょう。

値として保持できる
↓
値として保持

動的な所有権が必要
↓
所有者が1つ
↓
unique_ptr

複数が寿命を共有する必要がある
↓
shared_ptr

shared_ptr対象を所有せず参照する
↓
weak_ptr

スマートポインタでも防げない問題

スマートポインタを利用しても、ポインタに関するすべての問題が自動的に解決されるわけではありません。

shared_ptrの循環所有

複数のshared_ptrが互いを所有すると、オブジェクトが解放されないことがあります。

必要に応じてweak_ptrを使い、共有所有の循環を防ぐ必要があります。

get()で取得した生ポインタの誤用

get()で取得した生ポインタを手動でdeleteすると、二重解放などにつながります。

生ポインタを利用するときも、誰が所有者なのかを意識することが重要です。

同じ生ポインタから複数のshared_ptrを作らない

次のようなコードは危険です。

int* raw = new int(100);

std::shared_ptr<int> p1(raw);
std::shared_ptr<int> p2(raw);

p1p2は別々の制御ブロックを持つため、それぞれが同じオブジェクトを削除しようとして二重解放につながります。

共有したい場合は、1つのshared_ptrを作成してコピーします。

auto p1 = std::make_shared<int>(100);
auto p2 = p1;

スマートポインタはメモリ以外のリソース管理にも使える

スマートポインタは、単にnewで確保したメモリを管理するだけの機能ではありません。

カスタムデリータを指定すれば、特殊な方法で解放する必要があるリソースにも利用できます。

例えば、C標準ライブラリのFILEを管理する場合は次のように記述できます。

#include <cstdio>
#include <memory>

std::unique_ptr<FILE, decltype(&std::fclose)>
    file(std::fopen("test.txt", "r"), &std::fclose);

このようにすれば、スマートポインタが破棄された際にstd::fcloseを自動的に呼び出せます。

スマートポインタは、メモリ管理だけでなく、より広い意味でリソースの所有権を管理するためにも利用できます。

C++のスマートポインタの選び方

C++でスマートポインタを選ぶときは、まず「そのオブジェクトを誰が所有するのか」を考えることが重要です。

単純なオブジェクトなら、まず値として保持できないかを検討します。

動的な所有権が必要で、所有者が1つならstd::unique_ptrが基本です。

複数のオブジェクトが同じ対象の寿命を本当に共有する必要がある場合はstd::shared_ptrを使用します。

shared_ptrで管理されているオブジェクトを所有せずに参照する場合は、std::weak_ptrを利用します。

スマートポインタは、単にdeleteを書かなくてよくするための機能ではありません。

オブジェクトの所有権や寿命をコード上で明確に表現し、安全なリソース管理を実現することが、スマートポインタを利用する大きな目的です。

以上、C++のスマートポインタについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次