Skip to main content

前準備その2 スマポ

スマートポインタ入門

目的

この資料では、C++ の std::unique_ptr の基本的な使い方を学びます。

今回の目標は、スマートポインタを深く理解することではありません。

まずは、次のことが分かれば十分です。

new で作ったものは delete で消す必要がある
std::unique_ptr を使うと delete を自動でやってくれる
std::make_unique は unique_ptr 用の new のようなもの

この考え方を使って、あとで DirectXTK Audio の AudioEngineSoundEffect を安全に管理します。

この資料の位置づけ

この資料では、オーディオエンジンを作る前に必要になる std::unique_ptr を中心に扱います。

std::shared_ptrstd::weak_ptr は、複数の場所で同じデータを共有するときの発展内容です。今回のオーディオエンジン本体では、まず std::unique_ptr を使います。

Audioが作る
Audioが持つ
Audioが消す

この流れをはっきりさせるためです。


1. 生ポインタ版:new と delete

まずは、スマートポインタを使わない書き方を確認します。

例:Player クラス

#include <iostream>

class Player
{
public:
    Player()
    {
        std::cout << "Playerを作成しました\n";
    }

    ~Player()
    {
        std::cout << "Playerを削除しました\n";
    }

    void Attack()
    {
        std::cout << "攻撃しました\n";
    }
};

この Playernew で作ると、次のようになります。

int main()
{
    Player* player = new Player();

    player->Attack();

    delete player;
    player = nullptr;

    return 0;
}

ポイント

Player* player = new Player();

これは、メモリ上に Player を作っています。

作ったものは、最後に消す必要があります。

delete player;
player = nullptr;

この delete を忘れると、作ったものがメモリに残ったままになります。


2. delete を忘れると困る

次のコードはよくありません。

int main()
{
    Player* player = new Player();

    player->Attack();

    return 0;
}

new Player() で作ったのに、delete していません。

Player* player = new Player();

したら、基本的には、あとで

delete player;

が必要です。

小さいプログラムではすぐ問題が見えないこともあります。 しかし、ゲームエンジンのように長く動くプログラムでは、消し忘れが増えると危険です。


3. std::unique_ptr 版

std::unique_ptr を使うと、delete を自動で行えます。

必要な include

#include <memory>

std::unique_ptrstd::make_unique を使うには、<memory> が必要です。


unique_ptr で Player を作る

#include <iostream>
#include <memory>

class Player
{
public:
    Player()
    {
        std::cout << "Playerを作成しました\n";
    }

    ~Player()
    {
        std::cout << "Playerを削除しました\n";
    }

    void Attack()
    {
        std::cout << "攻撃しました\n";
    }
};

int main()
{
    std::unique_ptr<Player> player = std::make_unique<Player>();

    player->Attack();

    return 0;
}

このコードには delete がありません。

std::unique_ptr<Player> player = std::make_unique<Player>();

と書くと、Player を作って、unique_ptr が管理してくれます。

main 関数が終わると、player も自動的に片付けられます。

そのため、手動で

delete player;

を書く必要がありません。


4. 生ポインタ版と unique_ptr 版の比較

生ポインタ版

Player* player = new Player();

player->Attack();

delete player;
player = nullptr;

unique_ptr 版

std::unique_ptr<Player> player = std::make_unique<Player>();

player->Attack();

違い

書き方 作り方 消し方
生ポインタ new delete が必要
unique_ptr std::make_unique 自動で消える

5. まず覚えること

std::unique_ptr は、次のように考えると分かりやすいです。

自動で delete してくれる箱

例えば、次のコードでは、playerPlayer を管理しています。

std::unique_ptr<Player> player = std::make_unique<Player>();

player がなくなると、中に入っている Player も自動で削除されます。


6. 関数を呼ぶときは普通のポインタに近い

unique_ptr で管理していても、使い方は普通のポインタに似ています。

std::unique_ptr<Player> player = std::make_unique<Player>();

player->Attack();

playerPlayer を指しているので、メンバ関数を呼ぶときは -> を使います。

player->Attack();

これは、生ポインタ版と同じ感覚です。

Player* player = new Player();
player->Attack();

7. reset で手動削除もできる

基本的には自動で消えるので、毎回使う必要はありません。

ただし、途中で明示的に消したい場合は reset() を使えます。

std::unique_ptr<Player> player = std::make_unique<Player>();

player->Attack();

player.reset();

reset() を呼ぶと、中の Player が削除されます。

生ポインタ版で言うと、次の処理に近いです。

delete player;
player = nullptr;

8. 空かどうかを確認する

unique_ptr が何も持っていない場合もあります。

std::unique_ptr<Player> player;

この状態では、まだ Player は作られていません。

使う前に確認できます。

if (player != nullptr)
{
    player->Attack();
}

または、次のようにも書けます。

if (player)
{
    player->Attack();
}

9. get() で中のポインタを取り出す

unique_ptr が管理している中身を、普通のポインタとして渡したい場合があります。

そのときは get() を使います。

std::unique_ptr<Player> player = std::make_unique<Player>();

Player* rawPlayer = player.get();

get() は、中に入っているポインタを取り出します。

ただし、重要な注意があります。

Player* rawPlayer = player.get();

で取り出した rawPlayer を、次のように delete してはいけません。

// これはダメ
delete rawPlayer;

理由は、Playerunique_ptr が管理しているからです。

消す仕事は unique_ptr に任せます。


10. get() は DirectXTK Audio で使う

DirectXTK Audio では、SoundEffect を作るときに AudioEngine のポインタを渡します。

生ポインタ版では、こうでした。

DirectX::AudioEngine* audioEngine = new DirectX::AudioEngine();

DirectX::SoundEffect* sound = new DirectX::SoundEffect(
    audioEngine,
    L"Assets/Audio/test.wav"
);

SoundEffect の作成には、audioEngine が必要です。

unique_ptr 版では、audioEngine は次のように管理します。

std::unique_ptr<DirectX::AudioEngine> audioEngine;

そして、作成します。

audioEngine = std::make_unique<DirectX::AudioEngine>();

このままだと、SoundEffect に渡す普通のポインタがありません。

そこで get() を使います。

std::unique_ptr<DirectX::SoundEffect> sound;

sound = std::make_unique<DirectX::SoundEffect>(
    audioEngine.get(),
    L"Assets/Audio/test.wav"
);

ここで使っている

audioEngine.get()

は、audioEngine の中にある普通のポインタを取り出して渡しているだけです。

delete はしません。


11. オーディオエンジンでの使い方

今回のオーディオエンジンでは、次のような変数を使います。

#include <memory>
#include <unordered_map>
#include <string>
#include <filesystem>
#include <Audio.h>

using file_path = std::filesystem::path;

namespace
{
    std::unique_ptr<DirectX::AudioEngine> audioEngine_;
    std::unordered_map<std::string, std::unique_ptr<DirectX::SoundEffect>> sounds_;
}

AudioEngine を作る

bool Audio::Initialize()
{
    audioEngine_ = std::make_unique<DirectX::AudioEngine>();
    return true;
}

これは、生ポインタ版で言うと次の処理に近いです。

audioEngine_ = new DirectX::AudioEngine();

ただし、unique_ptr 版では delete を自動で行います。


12. SoundEffect を作る

音声ファイルを読み込む処理は、次のようになります。

bool Audio::Load(const std::string& name, file_path& filepath)
{
    if (audioEngine_ == nullptr)
    {
        return false;
    }

    sounds_[name] = std::make_unique<DirectX::SoundEffect>(
        audioEngine_.get(),
        filepath.c_str()
    );

    return true;
}

ここで大事なところ

std::make_unique<DirectX::SoundEffect>( ... )

SoundEffect を作っています。

sounds_[name] = ...

で、作った音を sounds_ に保存しています。

audioEngine_.get()

で、SoundEffectAudioEngine のポインタを渡しています。

filepath.c_str()

で、std::filesystem::path から DirectXTK Audio に渡すファイルパスを取り出しています。


13. 音を鳴らす

保存した音を鳴らす処理は、次のようになります。

void Audio::Play(const std::string& name)
{
    auto itr = sounds_.find(name);

    if (itr == sounds_.end())
    {
        return;
    }

    itr->second->Play();
}

sounds_ には、名前と音がセットで保存されています。

sounds_["shot"]

のように、名前で音を探せます。

見つかったら、次のように再生します。

itr->second->Play();

itr->secondstd::unique_ptr<DirectX::SoundEffect> です。

unique_ptr でも、メンバ関数を呼ぶときは -> を使えます。


14. 終了処理

unique_ptr を使っている場合、ひとつずつ delete する必要はありません。

void Audio::Release()
{
    sounds_.clear();
    audioEngine_.reset();
}

sounds_.clear()

sounds_.clear();

sounds_ の中に入っている SoundEffect をまとめて消します。

std::unique_ptr が入っているので、中の SoundEffect も自動で削除されます。

audioEngine_.reset()

audioEngine_.reset();

AudioEngine を削除します。

生ポインタ版で言うと、次に近いです。

delete audioEngine_;
audioEngine_ = nullptr;

15. 作る順番と消す順番

オーディオでは、作る順番が大事です。

AudioEngine を作る
↓
SoundEffect を作る

SoundEffectAudioEngine を使って作ります。

そのため、消すときは逆順にします。

SoundEffect を消す
↓
AudioEngine を消す

今回の Release() では、この順番にしています。

void Audio::Release()
{
    sounds_.clear();
    audioEngine_.reset();
}

先に sounds_ を消してから、audioEngine_ を消しています。


16. 今回覚えるまとめ

生ポインタ

Player* player = new Player();
player->Attack();
delete player;
player = nullptr;

unique_ptr

std::unique_ptr<Player> player = std::make_unique<Player>();
player->Attack();

AudioEngine

audioEngine_ = std::make_unique<DirectX::AudioEngine>();

SoundEffect

sounds_[name] = std::make_unique<DirectX::SoundEffect>(
    audioEngine_.get(),
    filepath.c_str()
);

get()

audioEngine_.get()

unique_ptr の中にあるポインタを取り出して、DirectXTK Audio に渡すために使います。

reset()

audioEngine_.reset();

中身を削除します。

clear()

sounds_.clear();

unordered_map の中身を空にします。

中身が unique_ptr なので、管理している SoundEffect も自動で削除されます。


17. 確認問題

問題1

次の生ポインタ版で、最後に必要な処理を書いてください。

Player* player = new Player();
player->Attack();

// ここに必要な処理を書く

問題2

次の空欄を埋めてください。

std::unique_ptr<Player> player = std::____________<Player>();

問題3

unique_ptr で管理しているオブジェクトのメンバ関数を呼ぶとき、どちらを使いますか。

player.Attack();
player->Attack();

問題4

次の get() の説明として正しいものを選んでください。

audioEngine_.get()
A. unique_ptr の中のポインタを取り出す
B. unique_ptr を削除する
C. 音を再生する

問題5

次のコードで、delete rawPlayer; をしてはいけない理由を説明してください。

std::unique_ptr<Player> player = std::make_unique<Player>();
Player* rawPlayer = player.get();

// delete rawPlayer; はしない

18. 解答例

問題1

delete player;
player = nullptr;

問題2

std::make_unique

問題3

player->Attack();

問題4

A. unique_ptr の中のポインタを取り出す

問題5

rawPlayer は、unique_ptr が管理している中身を一時的に取り出しただけです。

削除は unique_ptr が行うので、delete rawPlayer; はしません。