前準備その2 スマポ
スマートポインタ入門
目的
この資料では、C++ の std::unique_ptr の基本的な使い方を学びます。
今回の目標は、スマートポインタを深く理解することではありません。
まずは、次のことが分かれば十分です。
new で作ったものは delete で消す必要がある
std::unique_ptr を使うと delete を自動でやってくれる
std::make_unique は unique_ptr 用の new のようなもの
この考え方を使って、あとで DirectXTK Audio の AudioEngine や SoundEffect を安全に管理します。
この資料の位置づけ
この資料では、オーディオエンジンを作る前に必要になる 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";
}
};
この Player を new で作ると、次のようになります。
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_ptr や std::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 してくれる箱
例えば、次のコードでは、player が Player を管理しています。
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();
player は Player を指しているので、メンバ関数を呼ぶときは -> を使います。
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;
理由は、Player は unique_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()
で、SoundEffect に AudioEngine のポインタを渡しています。
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->second は std::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 を作る
SoundEffect は AudioEngine を使って作ります。
そのため、消すときは逆順にします。
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; はしません。
No Comments