初めに
最近、システムの構成を設計しながらコードを作成していました。その際、急にGetterを作成するのが面倒くさいなと思っていい方法があるのか考えてみました。templateとif-constexperを適用したら、別のGetter関数を宣言しなくtemplate関数だけ存在する簡単な形のコードになるはずだと判断しました。それで、templateを適用した際の利点は何があるかを記事として作成しました。
Unreal EngineとC++、SharedPtr
本論の前に少し昔で苦労をした覚えがありました。初めてUnrealEngineの勉強を始めた際、どうすれば純粋なC++を使うとこが出来るのかわかりませんでした。その上、Actorの機能が必要ないのにActor系のクラスで機能を実装する間違いがありました。
C++のクラスをUnreal Engineで作成する方法は簡単な事で、これをしらなかったことがはずかしいほどでした。単純にクラスを作成すればいいでした。
#pragma once
#include "CoreMinimal.h"
//実際にはhファイルですが、色がなくてcppで作成しました。
class FSampleClass : public TSharedFromThis<FSampleClass>
{
public:
FSampleClass();
~FSampleClass();
private:
//例です
TSharedPtr<ISystemName> nanikanosystem
};
今回の記事の例えとして作成したクラスで、クラスの内部にはいろんなシステムの変数を持っている構造です。CPPを作成した際と同じくPragmaを宣言してクラスを作成しています。差はTSharedFromThisを継承している形です。それはC++11で追加されたSmartPointerをUnrealEngineで改めて実装した機能です。
Unreal Smart Pointerの公式文書
「TSharedFromThis」を継承したクラスは「AsShared」、「SharedThis」二つのメソッドを活用できて、SharedRefの形で返還をしています。SharedRefとSharedPtrの差を簡単に説明すればNullになる可能性です。
- SharedPtr:SharedRefenceを所有して、保有しているオブジェクトがなければSharedPtrは消滅します
- SharedRefence:自分をSharedPtrの形式で返還することができます。返還された値がNullになる可能性が0になることを保証しています
動的に生成した変数を扱う際にSmart Pointerを採用するのは普遍的な方法だから試してください。
Getterのコード
//pragmaとincludeはスキップします
class FSystemCollector: public TSharedFromThis<FSystemCollector>
{
public:
FSystemCollector();
~FSystemCollector();
TSharedPtr<ISoundSystem> GetSoundSystem() const {return soundSystem;}
TSharedPtr<IFileSystem> GetFileSystem() const {return fileSystem;}
TSharedPtr<IContentsMangerSyste> GetContentsManagerSystem() const {return contentsManagerSystem;}
private:
TSharedPtr<ISoundSystem> soundSystem;
TSharedPtr<IFileSystem> fileSyste;
TSharedPtr<IContentsManagerSystem> contentsManagerSystem;
};
例えば、三つのシステムを所有しているFSystemCollectorというクラスがあれば、簡単にSharedPtrを返還しています。もし、システムの数が増えたら一対一で関数も増えることが明らかです。問題はありませんですが、面倒くさい感じがあることは事実です。Getter関数の数のせいでコードが汚い感じもあります。
このクラスへtemplateを適用したら下の例となります。
//pragmaとincludeはスキップします
class FSystemCollector: public TSharedFromThis<FSystemCollector>
{
public:
FSystemCollector();
~FSystemCollector();
template <typename InterfaceName>
TSharedPtr<InterfaceName> GetSystem() const
{
if constexpr (std::is_same_v<InterfaceName, ISoundSystem>)
{
return soundSystem;
}
else if constexpr (std::is_same_v<InterfaceName, IFileSystem>)
{
return fileSyste;
}
else if constexpr (std::is_same_v<InterfaceName, IContentsManagerSystem>)
{
return contentsManagerSystem;
}
else
{
return nullptr;
}
}
private:
TSharedPtr<ISoundSystem> soundSystem;
TSharedPtr<IFileSystem> fileSyste;
TSharedPtr<IContentsManagerSystem> contentsManagerSystem;
};
新しいシステムを追加する場合、新しい関数を宣言しなくて、GetSystem関数の内部で追加すればおわります。応用してSetterを作成することも可能です。
if-constexprを使えば、コードが最適化されて簡単な形式になります。
if-constexpr
C++17から追加された機能で、コンパイルタイムで条件を評価してFalseになった際コンパイラーから無視されてコンパイルされなく、そのままコンパイルが実行します。その上、ランタイムでCPUの計算が必要なくなって処理の性能も上昇します。
if-constexprの処理の例えば
//SoundSystemを取る際
FSystemCollector systemCollector;
systemCollector.GetSystem<ISoundSystem>();
template <typename InterfaceName>
TSharedPtr<InterfaceName /*= ISoundSystem*/> GetSystem() const
{
// こちはTrue
if constexpr (std::is_same_v<InterfaceName, ISoundSystem>)
{
return soundSystem;
}
// こちらからの条件は全部Falseになってコンパイルされなく、ぞんざいしないコードになります。
else if constexpr (std::is_same_v<InterfaceName, IFileSystem>)
{
return fileSyste;
}
else if constexpr (std::is_same_v<InterfaceName, IContentsManagerSystem>)
{
return contentsManagerSystem;
}
else
{
return nullptr;
}
}
//コンパイルされた結果
template <typename InterfaceName>
TSharedPtr<InterfaceName /*= ISoundSystem*/> GetSystem() const
{
return soundSystem;
}
templateでISoundSystemが入った場合の例です。ISoundSystemを扱うFSystemCollector::GetSystem<ISoundSystem>()のコンパイルが行った際、条件文を検査してTrueになる条件を見つかったら、それ以外のコードは無視されて条件文の中身である値を返還する部分だけ残っていることで判断されます。その結果、return soundSystemだけ残っている簡単なコードになります。
⚠️注意
前に説明した通り、条件がFalseになる場合はコンパイラーが無視しているから無視されたところにコンパイルエラーを行うコードがある際問題になります。
template <typename InterfaceName>
TSharedPtr<InterfaceName> GetSystem() const
{
// ...省略
else
{
// ⚠️ コンパイルエラーなしで成功する場合があります
nullptr;
}
}
もし、GetSystemを呼んでいるケースの中で、elseの場合に当たる場合がなかったらこのコードは問題なくコンパイルに成功しまいす。
まとめ
Getter関数を作成するのが面倒くさくって試した方法でしたが、templateに慣れてない方も練習として良い例だと思います。