前回の記事と同じく書いていきたいと思います。前回と同じくあくまで個人的な意見ですし、いったんスマートポインタを用いて実装したのをこの記事で公開した際は問題がいくつか見つかったので、詳しい人はアドバイスをお願いします。
一昨年のアドベントカレンダーにこんな記事がありました。
https://qiita.com/CdecPGL/items/f698c8d0f5e4c9036915
結構便利だなと思う一方で専用関数を必要としたり、使うときに、アスタリスクを付けなきゃいけなかったり専用関数でポインタを取得したりする必要があったり。不便なところがあるように思います。ただ、先述したように、スマートポインタを利用した版は結構問題が見つかったのでスマートポインタではなくデストラクタを使いたいと思います。
#1.僕なりの実装
そこで、c++の知識があんまりないなりに自分だったらどう実装するのかということを書いたものです。先述した通り、デストラクタを使ったものになっています。問題が多数見つかっているのでもし使う際は元記事の物にしておいてください。
12月23日にプログラムにエラーチェックが加わったのでたぶん似ないとは思いますが、使っている人がいれば変えといてください。
#include "dxlib.h"
#define DX_HANDLETYPE_GRAPH 1
#define DX_HANDLETYPE_SOUND 3
#define DX_HANDLETYPE_FONT 8
namespace DxLib
{
// ハンドルリスト構造体
struct HANDLELIST
{
int Handle; // ハンドル
void *Data; // データへのポインタ
struct HANDLELIST *Prev, *Next; // リストの一つ前と次の要素へのポインタ
};
// ハンドルの共通データ
struct HANDLEINFO
{
int ID; // エラーチェック用ID
int Handle; // 自身のハンドル値
int AllocSize; // メモリの確保サイズ
int *DeleteFlag; // ハンドル削除時に-1にする変数へのポインタ
#ifndef DX_NON_ASYNCLOAD
int ASyncLoadCount; // 非同期読み込み処理の対象となっている数
int ASyncLoadResult; // 非同期読み込み処理の結果
int ASyncDataNumber; // 非同期読み込み処理番号
volatile int ASyncLoadFinishDeleteRequestFlag; // 非同期読み込みが完了したらハンドルを削除するフラグ
#endif
HANDLELIST List; // ハンドルリストの一つ前と次の要素へのポインタ
};
extern int SubHandle(int Handle); // ハンドルを削除する
extern HANDLEINFO *GetHandleInfo(int Handle); // ハンドルの情報を取得する
}
template<int handletype>
class myunique {
private:
int handle;
int getHandleType(int handle) {
int DX_HANDLETYPE_MASK = 0x7c000000;
int DX_HANDLETYPE_ADDRESS = 26;
return (int)(((DWORD)handle & DX_HANDLETYPE_MASK) >> DX_HANDLETYPE_ADDRESS);
}
int checkHandle(int handle) {
if (getHandleType(handle) != handletype) return 0;
HANDLEINFO *hi = GetHandleInfo(handle);
if (hi == NULL) return 0;
return 1;
}
public:
myunique() {
handle = -1;
}
myunique(myunique&& m) {
std::swap(handle, m.handle);
}
myunique(int i) {
handle = i;
if (!checkHandle(handle)) handle = -1;
}
~myunique() {
if (!checkHandle(handle)) handle = -1;
if (handle > -1) {
SubHandle(handle);
handle = -1;
}
}
myunique& operator =(int&& i) {
if (!checkHandle(handle)) handle = -1;
if (handle > -1) {
SubHandle(handle);
}
handle = i;
if (!checkHandle(handle)) handle = -1;
return *this;
}
myunique& operator =(myunique&& m) {
std::swap(handle,m.handle);
return *this;
}
const bool operator ==(myunique m) {
return *m == *this;
}
const bool operator !=(myunique m) {
return *m != *this;
}
const int& operator *()
{
return *this;
}
operator int() {
if (!checkHandle(handle)) handle = -1;
return handle;
}
operator bool() {
if (!checkHandle(handle)) handle = -1;
return (handle>-1);
}
};
typedef myunique<DX_HANDLETYPE_GRAPH> DxlibImageHandle;
typedef myunique<DX_HANDLETYPE_FONT> DxlibFontHandle;
typedef myunique<DX_HANDLETYPE_SOUND> DxlibSoundHandle;
一昨年にあった記事では、二つの方法が示されてました。
- カスタムデリータを使用する方法
- ハンドルを保持するクラスを作成する方法
上記で示した僕の実装はそのどちらとも違います。そもそもスマートポインタを使っていません。
#2.僕なりの実装での元記事との違い
どうもコメント欄で指摘されましたがc++ではだめな可能性が高いパターンらしいですが、僕の実装での特徴として、
- 専用関数を使わない
- 関数に引き渡すときにアスタリスクがいらない
- すでにロードした画像も代入すればスマートポインタで管理できるようにできる
といった物があります。
例えば、
DxlibImageHandle h = LoadGraph("test.png");
DrawGraph(0,0,h,1);
といったように使います。型名を変更しただけで、後は通常と同じ利用方法なのに気が付くと思います。このような使い方ができたらいいなという方針のもとで実装してみました。動作確認してないですが、ほかにフォントや音も通常と同じく使えると思います。音に関しては後述する問題を解決する過程で非対応になる予定です。(dxlibの管理人さんの協力で、音のハンドルも対応させることに成功しました。)先述しましたが上にあげたものはどうもc++ではだめな可能性が高いパターンらしいですが、僕は便利に越したことはないと思っています。まあ世間的にはマイナーな考えだとは思います。まあ安全面に問題があるなら話は別なのでスマートポインタ版は消してデストラクタ版をメインになるように記事を書き換えたのですが。少なくとも指摘された問題は残っていますが多少は安全だと思います。
#3.判明している現状の問題点
~~一つ目は、同じハンドルを持つ変数を複数作れることです。これは同じハンドルに対し、作った回数だけ多重開放してしまいます。~~この問題は、新たに追加されたエラーチェックでなくなりました。同じハンドルを持つ変数を複数作ったとしても、一つハンドルを削除すれば、同じハンドルを持つ変数は―1を返すようになり、解放も行いません。
~~二つ目は、暗黙の変換が行われる可能性があることです。これは、一つ目と同じ問題を発生してしまう他、例えば画像なら自前でDeleteGraphを使ったらそれを知ることはできません。~~この問題は新たに追加されたエラーチェックでなくなりました。上記の通り、ハンドルを解放したら―1を返すようになり、自前で開放しても問題ありません。
三つ目は、LoadDivGraphは対応できませんでした。できたら対応させたいんですが。一応、上記でDrawGraphに渡して使ってもいいことからわかるように、intには暗黙に変換してくれるようにはしてあるのですが。( 暗黙のキャストはバグやエラーの原因になったりするという話を聞くのでそこが心配です。 悪い予感は当たりました。上に発生した問題は書いてあります。)できるのかわかりませんが、myunique[]をint[]に変換するように実装すればいいとは思いますが。ただその場合、myunique側のオーバーロードされた代入演算子は呼ばれないと思うんですよね。
二つ目は、=対応させてるんですけど、これ一度任意のハンドル代入した後に、別のハンドルを代入することができるんですよね。ということは、前に代入されていたハンドルは管理されないことになります。ただ、それを解決する方法がいい方法が思い浮かばないんですよね。この問題に関しては、仮の解決となっています。=で代入すると、以前入っていたハンドルを解放するようになりました。ただ、同じハンドルを持つ変数が複数ある時などちょっと面倒なことになりそうなので、あくまでも借り対応とさせていただきます。
以前このほかにハンドルの種類が判別できるかわからないというのがありましたが、判断できるだろうと調べて判明しました。
#4.一昨年の記事のコメント欄で指摘されてる問題点
コメントにある、moveを考慮に入れるということの意味がいまいちわからず、たぶんできてないと思います。
コメントで指摘されているstaticで定義した場合の二重開放問題がありましたが、上記で何度か書いた通り、外部で開放したときは―1になるので、Dxlib_End()の中で開放処理が行われることによって、-1になることで二重開放を防止できてるはずです。
#5.まとめ
前回の記事同様、間違いなどあれば教えてください。また、上記の問題点の解決策があれば教えてくれると嬉しいです。実際、コメントは結構勉強になりました。その結果、記事の内容は結構変更しました。問題も増えました。問題を解決できるよう、今後もコメント欄で指導お願いします。
#追記(12月6日)
コメント欄で指摘されて気が付いたんですけど、これスマートポインタ使う意味が無いですねこれ。とりあえずスマートポインタを使わない版を作ってみました。
ついでに、use変数がいらない気がしたのでuse変数を消してhandleから判断するようにしました。スマートポインタを使う版では確かなんかuse変数を使う理由があった気がずるので一応use変数を残しておきます。(忘れました。)ただデフォルトコンストラクタでは0ではなく―1にしておきました。0だともしかしたら使うかも。
その他整理できるところは整理して、きれいにしてみました。本当はDeleterも使わないようにしたいのですが、引数の数が違うとダメなんですよね。オーバーロードされてればDeleterは無くても大丈夫だとは思いますけどデフォルト引数っぽいんですよね。その場合はできないようです。
#追記(12月6日 2回目)
コメント欄で結構大きい問題が発生することが判明しました。その結果、記事の内容を大きく変更することになりました。とはいってもスマートポインタを使わないようにしたことはぶっちゃけスマートポインタを使いこなせない問題の先延ばしに過ぎないので、今後も勉強したいと思います。
#追記(12月13日)
改修版の試作がdxlibの管理人さんの協力もあり完成しました。使ったことが無い関数なうえに非公開関数より非公開な関数を使っているので、もう少し検証してからこの記事を更新したいと思います。今回の改修でエラーチャックを厳しくしたので間違って利用することはよっぽど裏道を使わない限りは大丈夫だと思います。
#追記(12月23日)
すっかり記事の更新を忘れていました。まだ検証はやっていませんが、おそらく大丈夫でしょう。エラーチャックが厳しくなったのでほとんどの問題は解決したと思います。もしいやこういう問題があるよというのがあれば言ってください。