インタラクティブ問題対策用のクラスを作った
競技プログラミングでしばしば出題されるインタラクティブ問題について、長らくデバッグとか大変……と思っていたので、それ用のクラスをDIYしてみました。
やりたいこと
インタラクティブ問題の動作確認を容易にしたい
vscode+gdbのデバッグモードで、ブレークポイントで止めて様子を見られるようにしたい
stdin から親問題のパラメータ(インタラクティブ問題の中で、対話相手が持っている隠された数字等)を入れたい
デバッグモードで動かしていたコードをそのまま Submit したい
言語はC++
2.の項目が特に大事で、インタラクティブ問題のコードを書いていていつも苦労するところ。デバッグモードだとstdinに直接値を入れられなくて(もしかしたらできるかもしれないけれど、知らない)、今まではデバッグライト等で確認するか、もしくはペナルティを怖れずsubmitしてしまうか。インタラクティブ問題はサンプルも一つくらいしか無いので、お祈りモードが結構ありました。
設計
ググると、USER側とINTERACTOR側のプログラムをそれぞれ作って、FIFOをいじって貼り合わせるようなサンプルがいくつか見つかったのですが、(参考:https://qiita.com/recuraki/items/d4f4c01f588c421c9ced とか、https://qiita.com/ktateish/items/d350e359d921d174fc28 とか)これだとデバッグモードが使えない(いや、デバッグモードの設定をちゃんと書けば使えるのだけれど、launch.json をインタラクティブ用に別途用意するのが大変そうなのと、INTERACTOR側もデバッグモード必要なのとで)ので、この方法は使えない。
そこで、USER側クラスとINTERACTOR側クラスを作り、そのインスタンスが交互にやりとりするのを main で管理すればいいのでは?そうすれば、どちら側でもブレークポイントで止められるので。
しかしこれだと4.の条件、つまり、これで作ったコードをコピペして Submit ができない、またはしにくい。提出したいのは USER 側だけで、本来 main 関数は USER 側にあってほしいし、例えば main を差し替えて Submit するにしても差し替えに少なくない作業が発生しそう。提出する USER 側は単独で取り出すと動かなくなるので、Submit用の作業が多くなればそれだけミスが混入しやすくなる。
そこで、コードの書き方は USER 側のみを作っているのとほぼ変わらずに、でもそれに INTERACTOR の動作を差し込めるようにしようと思って作ったのが次の形。
この形だと、問い合わせ先を INTERACTOR のインスタンスにして応答させればデバッグができるし、問い合わせ先を外部(cout で尋ねて、 cin で受け取る)に切り替えればそのまま提出できる。さらに、デバッグモードでは mainが最初に受け取る cin を INTERACTOR の初期化に突っ込んでやれば 3. の条件も対応できる。
さらに、デバッグモードと提出モードで問い合わせを切り替える際にミスしたくないので、これを共通化しようとすると、問い合わせに cout, cin を使いたい。そこで、INTERACTOR を ON にしたときは cout, cin の出力先を乗っ取って、USER が cout に出力したものを INTERACTOR が string で傍受して、その後、INTERACTOR が擬装応答文を作成し、USER が cin で受け取れるようにして渡してあげれば、USERは相手が INTERACTOR インスタンスなのか本番の対話相手なのかがわからない。C++STLにはちょうど、
std::streambuf *std::ios::rdbuf(std::streambuf *__sb)
というバッファ擬装用の関数があるので、これを使うと USER は cout に突っ込んでいるつもりでも、実際には cout に出ず、それを INTERACTOR 側で string で受け取ることができる。(なお、これは chatGPTに教えて貰いました。便利!)
問題は、INTERACTOR が行動するタイミングで明示的に動かしてあげないといけないこと。基本的には1スレッドで動くプログラムにしないとデバッグなどは容易でないので、1スレッドで動かしておいて、擬似的にUSERとINTERACTORが独自に動いているようにしたい。ただ、これは難しくなくて、INTERACTORは基本的にUSERが何か問い合わせしたときにしか応答しないので、USER が cout するたびに INTERACTOR の「応答」関数を呼び出してあげればOK。で、INTERACTORは呼び出されたら、デバッグモードならば応答分を作成し、USER 側が cin で受け取れるバッファに文字列を格納してから制御を戻せばいいし、もし本番モードなら何もしないで戻せばいい。制御がUSER 側に戻ってきたら、USER側は次の cin のところまで進んで待つが、もしデバッグモードなら INTERACTOR が既に cin バッファに文字列を入れているのでそれを取得するし、本番ならばホンモノの interactor が stdin に文字列を入れるまで待っていればよい。
ということで作ったINTERACTORのクラスがこちら。
#include <bits/stdc++.h>
using namespace std;
class INTERACTOR{
private:
bool MODE_; // true: テストモード, false: 提出モード
std::streambuf* oldCoutStreamBuf; // 従来のcoutのバッファを待避しておく場所
std::streambuf* oldCinStreamBuf; // 従来のcinのバッファを待避しておく場所
std::ostringstream toInteractor;
std::istringstream fromInteractor;
public:
// コンストラクタ
// テストモードなら true を、提出モードなら false を渡す。
INTERACTOR(bool mode): MODE_(mode){}
// 初期化し、必要ならば cin バッファに string を入れて戻る。
// mode が true なら cout バッファを切り替える
void initialize(){
if(!MODE_) return;
oldCoutStreamBuf = std::cout.rdbuf(); // 従来のcoutのバッファを待避しておく
std::cout.rdbuf(toInteractor.rdbuf()); // coutにossのバッファを設定
response_initialize(); // response_initialize 内では、外部からの stdin を cin で受け、cout は toInteractor でキャプチャする。
string sin = toInteractor.str(); // response_initialize() で cout され、toInteractor でキャプチャした文字列を取得
toInteractor.str(""); // toInteractor をクリア
oldCinStreamBuf = std::cin.rdbuf(); // 従来のcinのバッファを待避しておく
std::cin.rdbuf(fromInteractor.rdbuf()); // cinにissのバッファを設定,
fromInteractor.str(sin); // fromInteractor に sin をセットして、USER の cin で取得できるようにする。
}
void ask(){
if(!MODE_) return;
string sin = toInteractor.str(); // USERの cout を toInteractor でキャプチャしているので、その内容を取得
toInteractor.str(""); // toInteractor をクリア
fromInteractor.str(sin); // fromInteractor に sin をセットして、response_ask の cin で取得できるようにする。
if(!response_ask()) abort(); // response_ask で問い合わせを行い、その結果を取得。falseなら強制終了する。
fromInteractor.str(""); // fromInteractor をクリア
string sout = toInteractor.str(); // response_ask での cout を toInteractor でキャプチャしているので、その内容を取得
toInteractor.str(""); // toInteractor をクリア
fromInteractor.str(sout); // fromInteractor に sout をセットして、USER の cin で取得できるようにする。
return;
}
void finalize(){
if(!MODE_) return;
std::cout.rdbuf(oldCoutStreamBuf); // coutのバッファを元に戻す
std::cin.rdbuf(oldCinStreamBuf); // cinのバッファを元に戻す
response_finalize();
}
////////////////////////////////////////////////////////////////////////
// ここから、課題別の変数、関数を書く。
// 設定する関数:
// response_initialize(): インタラクタの初期化、初期条件設定
// response_ask(): インタラクタの問い合わせに対する応答
// response_finalize(): インタラクタの終了処理
////////////////////////////////////////////////////////////////////////
private:
// 課題個別の変数の追加
int sample;
// インタラクタの初期化、初期条件設定
void response_initialize(){
// この関数が呼ばれる時点では、まだ cin は標準入力になっているので、
// INTERACTOR 用の設定値などを cin から受け取ることができる。
cin >> sample;
// 他方、cout は既に INTERACTOR 用に切り替えられているので、
cout << sample << endl; // この出力は USER 側に渡される。
cerr << "sample = " << sample << endl; // cerr は擬装していないので、デバッグ用に使える。
return;
}
// インタラクタの終了処理
void response_finalize(){
// 終了時に何か作業が必要ならこれを実施する。
return;
}
// インタラクタの問い合わせに対する応答
bool response_ask(){
int a;
// INTERACTOR 側も、USERからの問い合わせを cin で受け取り、cout で USER 側に返すことができるようにしてある。
cin >> a; // 問い合わせ内容を受け取る
cout << a << endl; // 問い合わせ内容に対する応答を出力する
return true; // 通常終了ならtrueを返す。falseを返すとそこで強制終了する。
}
};
int main(){
INTERACTOR interactor(true); // true: テストモード, false: 提出モード
interactor.initialize(); // 初期化。ここで interactor は標準入力に入っている文字列を使って自身を初期化する。
int sample;
cin >> sample; // interactor が何か値を返していれば、cin でそれを受け取る。
int ans = 0;
for(int i = 0; i < sample; i++){
cout << i << endl; // 問い合わせる内容を cout に出力する
interactor.ask(); // interactor に制御を渡す
int j;
cin >> j; // interactor からの応答を cin で受け取る
ans += j;
}
interactor.finalize(); // interactor の終了処理。ここでバッファ乗っ取りが終わるので、これ以降は cout は stdout に出力される。
cout << ans << endl;
return 0;
}
下にくっつけてある main は提出用のコードを作る場所のサンプルで、最初に initialize を呼び、途中で INTERACTOR の応答がほしい時に ask を呼んでいる。具体的には、cout したら ask して、cin する。最後に finalize して終わり。
なお、コード中にもコメントしてありますが、INTERACTOR 側で書くのはclass定義の後半だけで、メンバ変数を追加し、response_initialize, response_ask, response_finalize の三つの関数の中身を書けば良いようにしてあって、かつ、INTERACTOR も USER からの問い合わせを cin で受け取り、応答を cout で返せるようにしてあります。これは、その中間で作業している人(ask関数)が双方向でバッファの内容を入れ替えて渡しているだけ。つまり、USER からもらった cout への出力をキャプチャして cin バッファに流し込んで INTERACTOR を呼び出し、INTERACTOR が cout した値をまたキャプチャして cin バッファに入れて処理を戻して、USER 側では cin で受け取れるようにしています。
あと、main の中の INTERACTOR のコンストラクタの引数に true を渡せばデバッグモードで、INTERACTOR が活性化しますが、false に変更すると INTERACTOR は何もしないので、これを切り替えることでそのまま本番提出用コードにできます。コンパイラのマクロ定義などと組み合わせれば、それも自動化できるかも。面倒だったのでやっていませんが。