はじめに
- C++のコンストラクタは、返り値を返せません。不便ではありませんか?私は不便と感じたため解決方法を20年模索し続けました。
状態遷移モデルで解決
まず結論から。全クラスが同じ状態遷移をする決まりにしました。そして状態遷移用のアクションとして、クラスにはコンストラクタとデストラクタの他に4つのメソッドを用意します。
// クラスは状態遷移のための6つのアクションをもつ
struct MyClass {
MyClass();
~MyClass();
bool init();
bool start();
bool stop();
bool finalize();
};
図の状態遷移をさせてクラスを運用します。コンストラクタとデストラクタのみだとPlain
とReady
とRunning
が同一状態となりますが、これが複雑さを増す原因となっています。そこで準備完了状態のReady
と、メイン機能がワークしている状態Running
を別状態とすることでシンプルに書けるようにしています。
説明
クラスはコンストラクタでインスタント化された状態となり、デストラクタで無に帰します。すなわち「無の状態」と「インスタント化された状態」の2状態が標準で存在します。
コンストラクタのみでクラス機能を立ち上げ完了しようとすると、返り値を使えず不便です。
また、機能の立ち上げ処理が重い場合などは、適切なタイミングになるまでコンストラクタを呼ばないように、optional<クラス>
でコンストラクタのタイミングを調整しないといけなくなります。
私はこの2状態を基本にすることに無理があると考え、4つに分割することでこの問題を解決しました。
- [Plain] 単にインスタント化しただけでまだ何も立ち上げ処理をしていない状態
- [Ready] リッチな立ち上げ処理を済ませた状態
- [Running] 機能開始している状態
- ([Destroyed] デストラクタが呼ばれ、メモリが開放された状態 = 「無の状態」)
状態遷移は前述の図の通りです。無の状態は開始終了状態として表現しています。
- 図中のアクションは以下の意味を持ちます。
- init / finalize
- ファイル操作やコネクションなど不確定要素のある処理
- start / stop
- メインの処理のオンオフ制御
- constructor / destructor
- エラーや例外を発生させない軽い処理
- init / finalize
- 備考
- 開始から終了への1方向ではなく、stop-->startやfinalize-->initも正しく処理される。
具体例
ネットワークにコネクトしてデータを待って処理する感じのサンプルコードを用意しました。ぜひ一部分でも真似して試してみてください。ルールがあることで全体がスッキリと書けるようになると思います。
私は大量にクラスを生み出していますが、80〜90%のクラスはこのモデルか、start/stopがない一部省略形で表現しています。
struct MyClass {
MyClass() = default;
~MyClass() = default;
bool init(const path& config, const string& host)
{
initialized_ = myRead(config) && myConnect(host) && myKickLoop();
return initialized_;
}
bool start()
{
if (initialized_ && !started_)
started_ = true;
return started_;
}
bool stop()
{
if (initialized_ && started_)
started_ = false;
return !started_;
}
bool finalize()
{
bool noErr = myJoinLoop();
noErr = myDisconnect() && noErr;
noErr = myWrite() && noErr;
initialized_ = false;
return noErr;
}
private:
bool initialized_{ false };
bool started_{ false };
bool eventLoop(const EventType& event)
{
if (initialized_ && started_)
myProcess(event);
}
};
このMyClass
クラスを、例えばこんな感じに利用します。
int main()
{
bool noErr = true;
MyClass engine;
noErr = engine.init("./config.json", "localhost");
noErr = noErr && engine.start();
if (noErr) {
// ...
}
noErr = engine.stop() && noErr;
noErr = engine.finalize() && noErr;
return (noErr ? 0 : 1);
}
この例のmyJoinLoop()
やmyDisconnect()
など終了系メソッドは、正常時は何度呼んでもダブって処理することのないメソッドをイメージしています。(コメントの指摘を受け修正しました。 2022/08/01)
以上です。
補足
- 上の例のようにクラス定義に
class
を使わないことが多いです。public:
の1行を書くのが無駄だからです。デフォルトがpublic
なstruct
が、この1点において圧倒的に優位だと思います。