ゲームプログラマのための設計シリーズ:実装詳細編の記事です。
概要
- ユーティリティ関数は非メンバ関数にしよう
- ただし、テンプレートを用いた静的Strategyパターンを用いるときは例外的に静的メンバ関数を利用する
本文
基本は非メンバ
算術計算など、使用目的を限定しないいわゆるユーティリティ関数の置き場としては以下の二か所が考えられます。
// 非メンバ関数版
namespace MathUtilityNS
{
int Add(int, int);
int Sub(int, int);
}
// 静的メンバ関数版
class MathUtilityClass
{
public:
static int Add(int, int);
static int Sub(int, int);
};
実装する側としては大した違いはないのですが、利用側としては名前空間ならそれを省略できるのでそちらを優先してあげたいです。
void func()
{
// 名前空間なら、usingで省略できる
using namespace MathUtilityNS;
Add(1, 2);
// 静的関数版はクラス名を省略できない
MathUtilityClass::Add(1, 2);
}
※余談:実装する側としての違いの例
- 非メンバ関数版はうっかりinlineをつけ忘れるかもしれない
namespace MathUtilityNS
{
int Add(int lhs, int rhs) { return lhs + rhs; } // inlineをつけてないのにインラインで定義を書くと、複数の.cppから参照されたときODR違反
}
class MathUtilityClass
{
public:
static int Add(int lhs, int rhs) { return lhs + rhs; } // メンバ関数はインラインで書いてもODR違反にならない
};
- メンバ変数を持つクラスの静的メンバ関数とする場合、そのクラスのprivate変数に触れる
class Vector2
{
using Self = Vector2;
float mX, mY;
public:
explicit Vector2(float x, float y) : mX(x), mY(y) {}
static Self Add(const Self& lhs, const Self& rhs)
{
// 静的であってもメンバ関数なのでprivateに触れる
return Self{
lhs.mX + rhs.mX,
lhs.mY + rhs.mY
};
}
};
- メンバ関数の実装を.cppに書く場合毎回クラス名::を書かないといけなくて面倒
静的メンバ関数が必要になるケース
テンプレート引数を差し替えることで、関数内の処理を部分的に切り替えるStrategyパターンというデザインパターンがあります。(動的ポリモーフィズムを利用する方法と比較して静的Strategyパターンと呼ばれたりします。Policyパターンと呼ばれることも)
これを利用したいは非メンバ関数ではどうにもならないので、静的メンバ関数を利用することになります。
// MathPolicyに与えたクラスのAdd/Sub関数を使って計算する関数
// C++20以降ではConceptを利用するのがベター
template<typename MathPolicy>
int calc(int a, int b, int c)
{
// AddとSubの実装を、テンプレート引数で切り替えられる
const int m = MathPolicy::Add(a, b);
const int n = MathPolicy::Sub(b, c);
return m * n;
}
void func()
{
const int s = calc<MathUtilityClass>(1, 2, 3);
const int t = calc<MathUtilityClass2>(1, 2, 3); // AddとSubの実装次第で違う結果になる
}
ハードウェアごとにそれぞれ最適な計算方法に丸っと切り替えたいなどといったケースで利用されますが、そのような場面はあまり多くはないでしょう。