たとえばこんなプログラムを実行するとどうなるだろうか?
int *a = new int;
delete a;
*a = 10;
deleteしたあとに変数aを使っているのでエラーになりそうな気がするが、
実は実行できてしまう。
そしてこれは結構恐ろしい問題で、delete済みなので変数aが指していたメモリの場所はもう使わないと思い、
別な変数が使っている可能性もある。
その状況で*aに10を代入すると、まったく関係ない変数の中身が10になってしまう可能性がある。
この問題を防ぐ方法は簡単で、deleteしたあとに必ずnullptrを代入してやればいい。
delete a;
a = nullptr;
nullptrは「0番地」という意味で、メモリの0番地は使えない仕様になっている。
だから、このあとに「*a=10」なんて書くとエラーになってくれる。
従って、これからはdeleteしたら必ずnullptrを代入するようにしよう。
・・・と言いたいところだが、続きも読んでくれ。
逆に、すでにアドレスがnullptrになっている場合はdeleteする必要がない。
つまり、完璧な開放処理は次のようになる。
if(a != nullptr)
{
delete a;
a = nullptr;
}
これが完璧な開放処理になる。
ということで、今後deleteを使うときはこの書き方にしよう。
・・・と言いたいところだが、毎回こんなこと書くのめんどくさいから続きを読んでくれ。
たかが開放処理のために毎回数行書くのは面倒なので、マクロにしてしまおう。
Direct3D.hに次のような宣言を追加する。(今のところDirect3D.hに書いておけば、みんなインクルードしてるからどこでも使えるようになる)
#define SAFE_DELETE(p) if(p != nullptr){ delete p; p = nullptr;}
こうしておけば、今後たとえば
SAFE_DELETE(abc);
と書けば
if(abc != nullptr){ delete abc; abc = nullptr;}
と書いたのと同じ意味になる。
では、WinMainの最後のところを直してみよう。
}
SAFE_DELETE(quad);
Direct3D::Release();
return 0;
}
これで完璧!
今後もdeleteを使うところがあったら、この方法にする。
Releaseで開放する方も同様に修正しよう。
<Direct3D.h>
#define SAFE_RELEASE(p) if(p != nullptr){ p->Release(); p = nullptr;}
<Quad.cpp>
Quad::~Quad()
{
SAFE_RELEASE(pConstantBuffer_);
SAFE_RELEASE(pIndexBuffer_);
SAFE_RELEASE(pVertexBuffer_);
}
ほかのReleaseを使っているところも直しておこう。
昨年の授業で「失敗する可能性がある処理を行う場合はassertを入れるように」と指示をした。
まだそれをやっていなかったので付けたそう。
assertを使うためにはassert.hのインクルードが必要。
<Direct3D.h>
#pragma once
//インクルード
#include <d3d11.h>
#include <assert.h>
現状のプログラムで一番問題が起きやすそうなところはHLSLファイル(シェーダーファイル)をロードするところだろう。
そこにassertを追加する。
void Direct3D::InitShader()
{
// 頂点シェーダの作成(コンパイル)
ID3DBlob *pCompileVS = nullptr;
D3DCompileFromFile(L"Simple3D.hlsl", nullptr, nullptr, "VS", "vs_5_0", NULL, 0, &pCompileVS, NULL);
assert(pCompileVS != nullptr);
pDevice->CreateVertexShader(pCompileVS->GetBufferPointer(), pCompileVS->GetBufferSize(), NULL, &pVertexShader);
:
:
:
// ピクセルシェーダの作成(コンパイル)
ID3DBlob *pCompilePS = nullptr;
D3DCompileFromFile(L"Simple3D.hlsl", nullptr, nullptr, "PS", "ps_5_0", NULL, 0, &pCompilePS, NULL);
assert(pCompilePS != nullptr);
pDevice->CreatePixelShader(pCompilePS->GetBufferPointer(), pCompilePS->GetBufferSize(), NULL, &pPixelShader);
pCompilePS->Release();
わざとファイル名を間違えてみて、正しく止まることを確認しておこう。
今までに『pDevice->Create〇〇〇』という関数がたくさん出てきた。
どれでもいいので、それらの一つにマウスカーソルを合わせてみよう。
すると、その関数のシグネチャ(何型の引数が何個あって、何型を返すか……みたいな関数の情報)が表示されるわけだが、ほとんどの戻り値の型がHRESULTとなっているのがわかる。
この型は、関数を実行した結果「上手くいったか失敗したか。失敗したならどんな理由で失敗したか」という情報が入る。もっと具体的に言うと、上手くいった(問題なく処理できた)場合は「S_〇〇」という定数が、失敗した場合は「E_〇〇」という定数が入る。これらの関数が「E_〇〇」を返したということは、何かを作成するのに失敗したため、これ以上プログラムを進めてもゲームを実行することができない。したがって、失敗したら理由に関係なくそこでプログラムを終了してしまうのが親切。そこで使えるのがFAILEDというマクロ。例えば、こんな風に使う。
HRESULT hr;
hr = pDevice->Create〇〇〇(・・・・);
if(FAILED(hr))
{
//失敗したときの処理
}
こうすれば、hrが「E_〇〇」のどれかになった時に、特定の処理を行うことができる。
上のコードをまとめて、このように書いてしまっても良い。
if(FAILED(pDevice->Create〇〇〇(・・・・)))
{
//失敗したときの処理
}
では、失敗したときに何をすれば良いだろうか。
例えば会社で仕事をしているとして、仕事中に何か問題が起きたらどうするだろうか。そういう時は「まず上司に報告」するのが正しい。自分判断だけで会社をストップするわけにはいかない。
つまり
HRESULT hr;
hr = pDevice->Create〇〇〇(・・・・);
if(FAILED(hr))
{
return hr;
}
return S_OK;
または
if(FAILED(pDevice->Create〇〇〇(・・・・)))
{
return E_FAIL;
}
return S_OK;
このようにする。
もちろん、こうするためには、この関数の戻り値の型をHRESULT型にする必要がある。
ちなみにE_FAILは「理由は知らんけど、とにかく失敗した」みたいな意味で、S_OKは「問題なく成功した」という意味。
例えば、Quad::Initialize関数では、頂点バッファ、インデックスバッファ、コンスタントバッファを作るときにHRESULT型のCreateBuffer関数を使っている。
ここで失敗したときにプログラムが止まるように修正してみよう。
まず、Quad::Initialize関数の戻り値をHRESULT型にする。
HRESULT Quad::Initialize()
{
もちろんQuad.hの方も変更する。
これで、何かあったらすぐ上司に報告できるようになった。
次に、各処理で失敗したらE_FAILを返すようにする。
HRESULT Quad::Initialize()
{
:
:
data_vertex.pSysMem = vertices;
if (FAILED(Direct3D::pDevice->CreateBuffer(&bd_vertex, &data_vertex, &pVertexBuffer_)))
{
return E_FAIL;
}
(以下2か所も同様に)
関数の最後まで行けば「全て問題なかった」ということなのでS_OKを返す。
HRESULT Quad::Initialize()
{
:
:
:
return S_OK;
}
さて、部下から「失敗した」と報告を受けた上司はどうすればいいだろうか。
その上司が係長や部長なら、同様のやり方でさらに上司に報告する。
今回はQuadクラスのInitialize関数を読んでいるのはMain関数で、会社なら社長に当たる。
この場合は社長判断でプログラムを止めてしまおう。
//エントリーポイント
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst, LPSTR lpCmdLine, int nCmdShow)
{
:
:
:
//Direct3D初期化
Direct3D::Initialize(winW, winH, hWnd);
if (FAILED(quad.Initialize()))
{
return 0;
}
こう書けば、Quad::Initializeが「失敗」の報告をしてくるとWinMainは即終了する。
WinMainが終わればプログラムが終了するという仕組み。
先ほど仕込んだエラー処理は、何かあったら即プログラムが終了するので、プレイヤーからすると何が起きたのかわからない。
そこで、メッセージボックスを表示させよう。
例えば、頂点バッファ作成処理のところに次の1行を追加する。
HRESULT Quad::Initialize()
{
:
:
:
D3D11_SUBRESOURCE_DATA data_vertex;
data_vertex.pSysMem = vertices;
if (FAILED(Direct3D::pDevice->CreateBuffer(&bd_vertex, &data_vertex, &pVertexBuffer_)))
{
MessageBox(nullptr, "頂点バッファの作成に失敗しました", "エラー", MB_OK);
return E_FAIL;
}
直前のデータをわざとおかしい値にして実行してみよう。
// 頂点データ用バッファの設定
D3D11_BUFFER_DESC bd_vertex;
bd_vertex.ByteWidth = sizeof(vertices);
bd_vertex.Usage = D3D11_USAGE_DEFAULT;
bd_vertex.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd_vertex.CPUAccessFlags = 0;
bd_vertex.MiscFlags = -1; ←テキトーに
bd_vertex.StructureByteStride = 0;
これで、何が起こったかわかりやすくなったはず。
(テキトーに値を変えたところはすぐ戻しておこう)
あとは、Create〇〇〇という関数を使っているところに同様の処理を付け足していこう。
これで安全なプログラムになる。
今後は、説明を簡単にするためにエラー処理を省略することもあるが、自分でエラー処理を追加していくように。
ちにみに、Initialize系の関数は最初から戻り値の型をHRESULT型にしておく。