超危険物 自己キュー型キューディスパッチャ

2020/04/23

C/C++ 技術

t f B! P L


皆様ご無沙汰しております。
Da★Boです

皆さんはコードレビュー好きですか?
ほら、人の書いたコードを見て処理がどうとか、コーディングルールに適合するかとか調べてコメントするあれですよ、あれ。

私?私は大ッキライですですよ。

というわけでレビューの修正待ちに時間が空いたので、今日はそんなストレスぶつけるのも兼ねて超危険物をあげますよ。

題して
自己キュー型キューディスパッチャwwww(名前つけてみたドヤァwwww)

はぁ・・・
そんな大層なもんじゃないですけどね、キューに積まれた処理を行って失敗したら失敗した処理をキューの末尾に積み直すだけの処理です。

---------追記:
修正版の本日リリース無理宣言食らっちゃった★

やってられっかー!
ドッカーン!

超危険物 再帰版ソースコード


解説

ポイント1:privateのメンバもdeque経由で呼べる

    if( !self_queue_.front()->managed_function_() )
        {

deque に登録しているのは自己クラスのインスタンスへのポインタなので、
deque 経由でそのインスタンスに登録した private メンバの関数オブジェクトを呼ぶことが出来ます。

今回はboolをトグルして返すだけの面白みもないサンプルコードを呼んでいます。

ポイント2:自己インスタンスへのポインタを持つdeque


std::deque\lt;SQ2D*\gy;                       self_queue_;

このクラスは自己インスタンスへのポインタをdequeで持っています。
まぁ、別にlistとかでも良いのですが

実行したい処理の関数オブジェクトを内包した自分のクラスを、
コンテナにして保持することで、頭から登録順にそれぞれのインスタンスが内包した処理を実行していきます。

ポイント3:失敗した処理は末尾へGo!


  void sets_self_queue()
    {
        self_queue_.push_back(self_queue_.front());
    }

FIFO のキュー処理なので、頭から処理していきます。
このとき処理をリトライしたい場合は自分のインスタンスをキューの末尾に再度加えます。

例えば通信のリトライのように失敗しても何度か試したい場合は、
このような構造にしたほうが一々ループで呼び出すよりも可搬性が高くなります。

ポイント4:多次元キューも何のその


    if( !self_queue_.empty() ) self_queue_.front()->dispatch();

        dispatch();

キューに積まれるのが自分と同じ構造のクラスのインスタンスであるため、
例えばキューが既に積まれたインスタンスのキューが既に積まれたインスタンスのキューを積み上げて(コピペミスではない)、
多次元処理を行うといった用途にも使えます。

まあそんなことをしてしまうとかなり追いにくくなるのでおすすめはしませんが。

それに今回は適当に短時間で適当に書いたため再帰を使っています。

ということはスタックメモリにガンガン情報が積まれるということでも有るので、
実際の用途ではイテレータパターンを使うなり、ループを使うなりしてディスパッチするのが現実的です。

使い方


  call_sample sample("sample1");
    call_sample sample2("sample2");
    call_sample sample3("sample3");
    SQ2D self_queue_sample(std::move(sample));
    SQ2D self_queue_sample2(std::move(sample2));
    SQ2D self_queue_sample3(std::move(sample3));
宣言して

self_queue_sample.sets_foreign_queue(&self_queue_sample);
    self_queue_sample.sets_foreign_queue(&self_queue_sample);
    self_queue_sample.sets_foreign_queue(&self_queue_sample);
キュー登録して

self_queue_sample.dispatch();
ディスパッチするだけ

sample1 called 1 times :0
sample1 called 2 times :1
sample1 called 3 times :0
sample1 called 4 times :1
sample1 called 5 times :0
sample1 called 6 times :1

同じ関数オブジェクトのインスタンスを共有しているので解りにくいですが、
最初の3回の呼び出しのうち、1,3回めが再度エンキューされて4,5回目のキューで呼ばれます。
そして5回目の呼び出しがさらにエンキューされて6回目の呼び出しが発生します。

多次元キューの実行


self_queue_sample2.sets_foreign_queue(&self_queue_sample2);
    self_queue_sample.sets_foreign_queue(&self_queue_sample);
    self_queue_sample.sets_foreign_queue(&self_queue_sample2);
    self_queue_sample.sets_foreign_queue(&self_queue_sample3);
内包している関数オブジェクトのインスタンスが異なったとしても、このクラス自体が同じものなので、
例えば複数インスタンスを一つのインスタンスに纏めて呼び出させることが出来ます。

self_queue_sample.dispatch();
呼び方はディスパッチするだけ

sample1 called 7 times :0
sample2 called 1 times :0
sample2 called 2 times :1
sample2 called 3 times :0
sample3 called 1 times :0
sample1 called 8 times :1
sample2 called 4 times :1
sample3 called 2 times :1
初めにキューに積まれた sample1 が実行されます。
次にsample2の実行にはいるのですが、sample2には既にひとつキューが積まれている状態で、
sample1のキュー上にスタックされています。

sample2にスタックされたキューが sample2上でまず呼ばれ(called1) 、それがsample2に再スタックされ再試行されます(called2)。

次にsample1に積まれていたsample2が実行され(called3) それも再スタックされcalled4で実行されます。

sample1とsample3の実行順は結果に基づきそのままです。

危険物 イテレータ版ソースコード


解説

再帰でやっていた初めのパターンの場合、キューの数だけスタックメモリが消費されてしまう問題がありました。
大したキューの乗らないささっと組んで終わらせたいアプリケーションであればそれでもまぁ目をつぶってやり過ごしても良いのですが、
ある程度キューの積まれる数が多いことが想定される場合は問題となります。

これを解決するためには、イテレータを使って実装してやる必要があります。

ポイント1:え、なにこのチェック

if(*it != this && 
            !(*it)->self_queue_.empty())
            {
                (*it)->self_queue_.front()->dispatch();
            }
キューに積まれるのは自分のインスタンスアドレスであったり、別のインスタンスアドレスであったりします。
ですから別次元のキューをディスパッチするには自己インスタンスのポインタとキューに積まれているインスタンスのポイントが異なるものであることを確認しなければ、
無限に自己インスタンスを呼び出してしまいます。

また同時にサンプルコードのsample3のように、別次元のキューが積まれていないインスタンスも積みます。
このような場合は空のキューを突付きに言ってプログラムがクラッシュします。

これらの事象を防ぐために、このようなチェックを施した上で別次元のインスタンスのディスパッチを呼びます。

ポイント2:なんで範囲forじゃないねん!

for(auto it = self_queue_.begin();
            it != self_queue_.end();
            ++it)
範囲for文は静的に展開されて処理が行われるため、for文中でpush_backにより末尾に要素が加えられても、ループ開始時の終点までしかイテレータが追ってくれません。

動的にキューの積み直しをしたい場合は昔ながらのfor文で、終了条件を一々チェックしながらループしてやる必要があります。

あとはほぼ一緒です。

ポイント3:スタックメモリの消費は抑制される!

再帰を使った実装の場合、スタックメモリはキューの数だけ消費されました。
しかしイテレータとループを使用して実装することで、スタックメモリを消費した関数呼び出しは、別次元のキューを探しに行く場合のみ、つまりキューの次元数文しか消費されません。

それでも例えば5000次元もキューを重ねればクラッシュしてしまう可能性はもちろん有るのですが、キュー5000個じゃなくて5000次元*n個のキューですからね。
実用上は十分なはずです。

ポイント4:無限ループには注意!

再帰版の実装も同様なのですが、繰り返し処理の終了条件を動的に、しかも繰り返し処理の中で書き換える危険な処理です。

実装が、というよりも処理の思想自体が危険です。
明確に処理すれば減ると言い切れるようなキューでない限りは、このような実装は避けたほうが良いでしょう(じゃあ書くな)

実行結果

sample1 called 1 times :0
sample1 called 2 times :1
sample1 called 3 times :0
sample1 called 4 times :1
sample1 called 5 times :0
sample1 called 6 times :1
---------------
sample1 called 7 times :0
sample2 called 1 times :0
sample2 called 2 times :1
sample2 called 3 times :0
sample3 called 1 times :0
sample1 called 8 times :1
sample2 called 4 times :1
sample3 called 2 times :1
挙動は異なりますが実行結果は再帰版と一緒です。

さいごに

むしゃくしゃしたので作った、後悔はしていない。

今回のサンプルコードはどちらも下手な実行を行うと危険なので、そのまま使えるものではありません。

ただ、キューに基づき処理をして、結果に基づき再度自己インスタンスをキューに載せるという処理のしかたは、終了条件さえしっかりと設計できていれば、動的な処理順の書き換えを非常に簡単に実現できる方法でもあります。

ぜひとも遊びの上で実装、使用してみてください。

Translate

ページビューの合計

注意書き

基本的にごった煮ブログですので、カテゴリから記事を参照していただけると読みやすいかと存じます。

ADBlocker等を使用していると、Twitterやアクセスカウンタが表示されません。

記事を読むには差し支えませんが、情報を参照したい場合には一時例外にしていただけると全てご参照いただけます。

Featured Post

ボイドラDICEの攻略法

QooQ