👏

[C++]ありとあらゆるフラグを保存できる最強クラスを作ってみる

2023/10/06に公開
1

初めに

私は現在専門学校にてC++を用いてゲーム開発を行っています。
開発している際にどうしても分散して存在してしまう外部からいじられる前提で作られるフラグの数々……
影響させる何かしらが登場するまで無意味に存在するフラグと判定処理……
私はどうしてもこれらが好きになれません。
そして思いつきました。「影響を与える側がその『影響』を定義して一か所に登録すればまだましになるかもしれない」と。
というわけで作っていきます。

必要な前提知識

  • ある程度のC++の知識

    具体例
    • template
    • std::unordered_map
    • std::typeindex(識別用 他の効率良い方法があればそっちでもいい)
  • 基礎的なbit演算の理解(フラグの本体)

    使うbit処理の軽い解説
    • 1<<3 0001(b)を三つ左シフト->1000(b) フラグの定義に使用
    • A | B or演算子 「AまたはB」1011 | 0001 ...〇 「どっちか」
    • A & B and演算子 「AかつB」 1011 & 0101 ...× 「どっちも」
    • ~ not演算子 !とほぼ同じ ~1011 =0100    「逆」
    • A |= B 1100 |= 0101 = 1101 フラグに足す
    • A &= ~B 1101 &= ~0101 = (1101 &= 1010) = 1000 フラグを消す

まぁ何とかなる。

構造概要

様々なクラスが定義しているbitフラグを一か所に集約させ定義した型毎にbitフラグを管理する。


パフォーマンス

メモリ

各自にboolで持たせた場合フラグ一つにつき1バイト。
集約させた場合一つのフラグの型につき4バイトで最大フラグを32個管理可能。
ただし可変長で確保するためもう少しメモリは食われる。
単純に考えれば4つ以上フラグができる可能性があるなら集約させた方がお得となる。

探索時間

std::unordered_mapに格納しているため
O(n)となる。
とんでもない数フラグを登録しない限り高速で動作するであろう。

ベースコード

実際にプログラムを書いていきます。まずは保存先の仕様を決めます。

FlagCollector
#include<unordered_map> //保存する可変長マップ配列
#include<typeindex>     //型の文字列化に使用

class FlagCollector{
	public:

	private:
				   //型名称,フラグ本体
	std::unordered_map< std::type_index , unsigned int > m_flags;
	}

フラグ追加関数追加

続いてフラグの登録関数を作成します。

FlagCollector
public:

	template<typename E>    //テンプレートはよくTが使用されますがenumの使用を前提としているのでこの記事ではEを使用します。
	void AddFlags(const unsigned int a_flags){
		if(!m_flags.count(typeid(E))m_flags[typeid(E)] = 0; //おそらく必要はないが念のための0リセット
		m_flags[typeid(E)] |= a_flags;
	}

SampleMain

enum SampleFlag{
	A=1<<0,  //0001(b) 1(d)
	B=1<<1,  //0010(b) 2(d)
	C=1<<2,  //0100(b) 4(d)
	D=1<<3,  //1000(b) 8(d)
	}

void main(){
	FlagCollector flag;
	flag.AddFlag<SampleFlag>(SamleFlag::A);                //フラグAをオンに
	//flag.m_flags["SampleFlag"] = 0001(b)
	flag.AddFlag<SampleFlag>(SampleFlag::B|SampleFlag::D); //フラグB,Dをオンに
	//flag.m_flags["SampleFlag"] = 1011(b)
	flag.AddFlag<SampleFlag>(SamleFlag::A);                //フラグAをオンに
	//flag.m_flags["SampleFlag"] = 1011(b) すでに立っているフラグは変わらない
	}

bitで保存しているため同時にいくつものフラグをいじれるのも利点の一つです。
同時にいじるなんてそうそうありませんが。


フラグ削除関数追加

FlagCollector
public:
//Add~~

	template<typename E>
	void DeleteFlags(const unsigned int a_flags){
		if(!m_flags.count(typeid(E))return;
		m_flags[typeid(E)] &= ~a_flags;
	}
SampleMain

enum SampleFlag{
	A=1<<0,  //0001(b) 1(d)
	B=1<<1,  //0010(b) 2(d)
	C=1<<2,  //0100(b) 4(d)
	D=1<<3,  //1000(b) 8(d)
	}

void main(){
	FlagCollector flag;
	flag.AddFlag<SampleFlag>(SamleFlag::A|B|C|D);   //フラグA,B,C,Dを追加
	//flag.["SampleFlag"] = 1111(b)
	flag.DeleteFlags<SampleFlag>(SampleFlag::A);    //フラグAを削除
	//flag.["SampleFlag"] = 1110(b)
	flag.DeleteFlags<SampleFlag>(SampleFlag::B|D);  //フラグA,Bを削除
	//flag.["SampleFlag"] = 0100(b)
	flag.DeleteFlags<SampleFlag>(SampleFlag::A);    //フラグAを削除
	//flag.["SampleFlag"] = 0100(b)	すでに消えているフラグは変わらない
	}

これで基礎的なフラグを立てたり消したりする処理は完成しました。


判定関数追加

登録されていなくても問題なく判定できるという点が最高に良いです。
bit演算なのもあって同時に複数判定できます。
いずれか一つでも満たしている場合と全て満たしている場合の2つ作ります。

FlagCollector
public:
	//他関数...
	
	//引数の条件をすべて満たしているか
	template<typename E>
	bool IsEnabled(const unsigned int a_flags) const {
		if(!m_flags.count(typeid(E))return false; //登録されていなかったらfalse
		return (m_flags[typeid(E)] & a_flags) == a_flags;
	}
	
	//引数の条件いずれかを満たしているか
	template<typename E>
	bool OrEnabled(const unsigned int a_flags) const {
		if(!m_flags.count(typeid(E))return false;
		return m_flags[typeid(E)]&a_flags;
	}
SampleMain
enum SampleFlag{
	A=1<<0,  //0001(b) 1(d)
	B=1<<1,  //0010(b) 2(d)
	C=1<<2,  //0100(b) 4(d)
	D=1<<3,  //1000(b) 8(d)
	}
void main(){
	FlagCollector flag;
	flag.AddFlags<SampleFlag>(SampleFlag::A|B);
	//flag.m_flag["SampleFlag"] = 0011(b)
	bool result;
	result=flag.IsEnabled<SampleFlag>(SampleFlag::A);
	//result = true
	result=flag.IsEnabled<SampleFlag>(SampleFlag::A|B);
	//result = true
	result=flag.IsEnabled<SampleFlag>(SampleFlag::A|C);
	//result = false
	
	result=flag.OrEnabled<SampleFlag>(SampleFlag::A);
	//result = true
	result=flag.OrEnabled<SampleFlag>(SampleFlag::A|B);
	//result = true
	result=flag.OrEnabled<SampleFlag>(SampleFlag::A|C);
	//result = true	
	}

追加、削除、判定ができるようになったので限りなく完成です。
あとはおまけ程度に使えるかも関数を足していきます。


おまけ関数たち

boolを受け取ってその値でフラグを入れる関数
FlagCollector
public:
        //他関数...
        template<typename E>
	void SetFlags(const unsigned int a_flags,const bool a_enable){
		a_enable ? AddFlags<E>(a_flags):DeleteFlags<E>(a_flags);
	}
フラグを反転させる関数
FlagCollector
public:
        //他関数...
	template<typename E>
	void ReversFlags(const unsigned int a_flags){
		if(!m_flags.count(typeid(E))m_flags[typeid(E)] = 0; //念のため
		m_flags[tupename(E)] ^= a_flags;
	}        
全部0リセットする関数
FlagCollector
public:
        //他関数...
        template<typename E>
	void ResetFlags(){
		m_flags[typename(E)]=0;
	}
2進数でフラグを返す関数(つかったことないです)
FlagCollector
public:
        //他関数...
        template<typename E>
	unsigned int GetFlags() const {
		if(!m_flags.count(typeid(E))return 0;
		return m_flags[typename(E)];
	}

完成!!(統合コード)

以上です!!すべてをまとめたコードはこちらです!

FlagCollector
#include<unordered_map> //保存する可変長マップ配列
#include<typeindex>     //型の文字列化に使用

class FlagCollector{
	public:
		template<typename E>    //テンプレートはよくTが使用されますがenumの使用を前提としているのでこの記事ではEを使用します。
		void AddFlags(const unsigned int a_flags){
			if(!m_flags.count(typeid(E))m_flags[typeid(E)] = 0; //おそらく必要はないが念のための0リセット
			m_flags[typeid(E)] |= a_flags;
		}
		template<typename E>
		void DeleteFlags(const unsigned int a_flags){
			if(!m_flags.count(typeid(E))return;
			m_flags[typeid(E)] &= ~a_flags;
		}
		//引数の条件をすべて満たしているか
		template<typename E>
		bool IsEnabled(const unsigned int a_flags) const {
			if(!m_flags.count(typeid(E))return false; //登録されていなかったらfalse
			return (m_flags[typeid(E)] & a_flags) == a_flags;
		}
	
		//引数の条件いずれかを満たしているか
		template<typename E>
		bool OrEnabled(const unsigned int a_flags) const {
			if(!m_flags.count(typeid(E))return false;
			return m_flags[typeid(E)]&a_flags;
		}
		
		template<typename E>
		void SetFlags(const unsigned int a_flags,const bool a_enable){
			a_enable ? AddFlags<E>(a_flags):DeleteFlags<E>(a_flags);
		}
		
		template<typename E>
		void ReversFlags(const unsigned int a_flags){
			if(!m_flags.count(typeid(E))m_flags[typeid(E)] = 0; //念のため
			m_flags[tupename(E)] ^= a_flags;
		}   
		
		template<typename E>
		void ResetFlags(){
			m_flags[typename(E)]=0;
		}
		
		template<typename E>
		unsigned int GetFlags() const {
			if(!m_flags.count(typeid(E))return 0;
			return m_flags[typename(E)];
		}
	private:
					  //型名称,フラグ本体
		std::unordered_map< std::type_index , unsigned int > m_flags;
	}

おまけ(デバッグに)

一か所にフラグが集まっているおかげでデバッグの仕組みさえ作れば全てのフラグを可視化できそう。そこでおすすめするのが、NameOf
なんとフラグを文字列変換してくれるので、hogeFlag : A|B|D みたいな描画が可能なのだ。
デバッグしやすくなるであろう。たぶん。

懸念点

抱えている人為的ミスとなりうる問題点がいくつか存在しているのだけ注意が必要かもしれない。

  • templeteの型を間違えても登録、判定を行えてしまう
  • 判定に使用する引数がunsigned intなので1とかの実数値を入れたり別のフラグの数値を入れても動いてしまう

エラーチェックはチョットオモイツカナカッタノデ有識者頼む、、、

あとがき

不具合とか改善点とかあればぜひとも教えていただきたく、、、、
精進します!!!

Discussion

ぷらむらいす(PlumRice)ぷらむらいす(PlumRice)

分かりやすく、よい記事をありがとうございます!

WindowsAPIでもよく使われている古典的で有名な手法ですね。
TYPEFLAGS enumeration (oaidl.h)とかのページを見ると良く載ってます。

WindowsAPIで、普通に使うもので、一番身近なAPIだとしたらCreateWindowあたりのウィンドウ系だと思いますが、CreateWindowExでも、拡張ウィンドウ スタイルなどといったフラグ管理で使われていますね。

個人的な思想ですが、WindowsAPIで用いられているように、1つの状態を複雑に表現したいという用途以外で、1つの変数に多くの情報を含ませるのは、やめておいた方がいいと考えています。
20年以上昔の機器を扱うならメモリなどの関係で使用制限が掛かるので、このようにする利点があるとは思いますが、現代の一般的な機器の場合でのプログラミングならば、こういう手法ばかりを取ろうとすると、読みづらい理解しづらいソースコードになったり、懸念点として上げたくださった内容の問題が出るので、1つの変数にたった数個程度の情報ぐらいしか組み込めないならば、ごく普通に変数を作った方がいいと思っています。
ちなみに、もし私ならば、monsterクラスを継承したdragonクラスやzombieクラスなどを作成、typeidなど型判別ができるような手法を使い、dragonクラスのflag2を知る。という、とても単純なプログラムを書くと思います。