
【番外編】Brainfuckを実装しながら学ぶC++【改造編】
2021.12.20
番外編~Brainfuckを実装しながら学ぶC++
さて前回で、「Brainfuckを実装しながら学ぶC++」シリーズのメインは、終わりを迎えましたね!
今回は番外編ということで、出来上がったコードを、少しだけ改造してみたいと思います。
皆さんのオリジナルの Esolang 作成、または C++ 学習の一助となれば幸いです。
命令を変えてみよう
まずは、一番簡単なアレンジです。
1 2 3 4 5 6 7 8 | constexpr char INCREMENT = '+'; constexpr char DECREMENT = '-'; constexpr char RIGHT = '>'; constexpr char LEFT = '<'; constexpr char LOOP_START = '['; constexpr char LOOP_END = ']'; constexpr char OUTPUT = '.'; constexpr char INPUT = ','; |
の部分を好きなものに変えてみましょう。
例えば、
1 2 3 4 5 6 7 8 | constexpr char INCREMENT = 'a'; constexpr char DECREMENT = 'b'; constexpr char RIGHT = 'c'; constexpr char LEFT = 'd'; constexpr char LOOP_START = 'e'; constexpr char LOOP_END = 'f'; constexpr char OUTPUT = 'g'; constexpr char INPUT = 'h'; |
と変えるだけでも、オリジナルの Esolang になりますね!
ただし、 char 型は1バイト文字なので、文字列は指定できません。
さらに、C / C++ の switch 文は、「整数」もしくは今回のような「1バイト文字」しか分岐が行えません。
この文字列を使った、Esolang 作成の対処については、後半で紹介します。
switchではなくmapを使う
実は、 switch を使わずとも、Brainfuck インタプリタは作成できます。
その場合、 std::map<> を使います。
これは、Python の辞書型変数のように扱うことができる、便利な STL (Standard Template Library) の一つです。
mapの使い方
まずは、インクルードしましょう。
1 | #include <map> |
これで使えるようになります。
キーは、どんな型でも良いのですが、例えば以下のように扱います。
1 2 3 4 5 6 7 | std::map<std::string, int> dict = { {"Tokyo", 1395}, {"Osaka", 882}, } std::cout << map["Osaka"] << "万人" << std::endl; // > 882万人 |
stringをキーとして関数を呼ぶ
map を使った実装では、例えば「 "+" をキーとしたら、 increment() を呼ぶ」のように実装する必要があります。
つまり、以下のように呼び出せるのが理想です。
1 | bfs["+"](); // increment()が呼ばれる |
こういった処理は、関数ポインタを利用して、実装する手段があります。
関数ポインタとは、その名の通り関数へのポインタです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include <iostream> #include <map> using namespace std; void increment(){ cout << "Called Increment function." << endl; } void decrement(){ cout << "Called Decrement function." << endl; } int main(){ map<string, void (*)()> bfs = { {"+", increment}, {"-", decrement}, }; bfs["+"](); // > Called Increment function. bfs["-"](); // > Called Decrement function. return 0; } |
このように、関数ポインタは void (*) のように、括弧が必要です。
なぜ括弧が必要かと言うと、こういうことです。
1 2 3 4 5 | void* i_ptr() = &increment; // [エラー] これだと void* が戻り値の関数とみなされる void (*d_ptr)() = &decrement; // こっちだとd_ptrがポインタだと解釈される i_ptr(); // エラー d_ptr(); // >Called Decrement function. |
ややこしいですね…
ちなみに、その後ろの括弧は引数です。
Brainfuckインタプリタに適用してみる
早速、関数ポインタと map を駆使して、インタプリタを再構築してみましょう!
まずは、各命令に対して、関数を定義します。
このとき、全ての関数について「ポインタ」や「メモリ」は弄る対象となるので、参照渡しで定義しましょう。
中身は、前回実装したものと同じです。
まずは、関数部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | /* 省略 */ #include <map> /* 省略 */ void increment (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ memory[ptr]++; } void decrement (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ memory[ptr]--; } void right (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ ptr = (ptr >= MEMORY_SIZE-1)? 0: ptr + 1; } void left (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ ptr = (ptr <= 0)? MEMORY_SIZE-1: ptr - 1; } void output (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ putchar(memory[ptr]); } void input (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ memory[ptr] = getchar(); } void loop_start(unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ loops.push(code_ptr); // "["の位置を保存 /* ポインタの値が0ならば "]" に飛ぶ */ if (memory[ptr] == 0) { int depth = 1; //! ループの深さ // 対応する "]" を探す while (depth > 0){ code_ptr++; // コードポインタを進める // もしコード終端に来てしまったらエラー if (code_ptr >= code_len){ cerr << "Error: Cannot find \"]\"." << endl; exit(-1); } // "[" と "]" を見つけたら深さを修正 if (code[code_ptr] == LOOP_START) depth++; if (code[code_ptr] == LOOP_END) depth--; } // もしdepth=0であれば、対応する "]" が見つかったので抜ける loops.pop(); // ループ終わり } } void loop_end (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ if(loops.empty()){ cerr << "Error: Loop start order, " << LOOP_START << ", is not found." <<endl; exit(-1); } code_ptr = loops.top() - 1; // 対応する "[" に移動 loops.pop(); // 飛んだらまたスタックされるのでポップしておく } int main(int argc, char* argv[]) { /* ... */ } |
これらを格納する、map を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | int main(int argc, char* argv[]) { unsigned char memory[MEMORY_SIZE]; //! 1Byte=8bit (0~255) で構成されるメモリを定義 unsigned int ptr = 0; //! メモリポインタ (負の値は取らないのでunsigned) unsigned int code_ptr = 0; //! Brainfuckコードのポインタ unsigned int code_len = 0; //! Brainfuckコードの終端 (長さ) stack<int> loops; //! ループのネスト用スタック: "[" のコードポインタが格納される /* [new] Brainfuck orders */ map<char, void(*)(unsigned char*, unsigned int&, unsigned int&, string&, stack<int>&, unsigned int&)> bfs = { {INCREMENT, increment}, {DECREMENT, decrement}, {RIGHT, right}, {LEFT, left}, {OUTPUT, output}, {INPUT, input}, {LOOP_START, loop_start}, {LOOP_END, loop_end}, }; /* 省略 */ } |
このように定義することで、Brainfuck 解析部分の while 文の中は、以下のようにかなりスッキリしますね!
1 2 3 4 5 6 7 8 | /* Brainfuck 解析 */ while (code_ptr < code_len){ // もしキーが存在すれば if (bfs.count(code[code_ptr]) > 0){ bfs[code[code_ptr]](memory, ptr, code_ptr, code, loops, code_len); } code_ptr++; } |
ちなみに、map を使った方が、コードの管理はしやすいかもしれません。
ですが、switch の方が、実行速度が速いのです。
文字列を命令にする
map を利用すれば、文字列を命令とした、Esolang が作成できるようになります。
命令を文字列へ
1 2 3 4 5 6 7 8 9 10 11 | // 命令をコンパイル時定数として定義 constexpr char* INCREMENT = (char*) "abc"; constexpr char* DECREMENT = (char*) "def"; constexpr char* RIGHT = (char*) "ghi"; constexpr char* LEFT = (char*) "jkl"; constexpr char* LOOP_START = (char*) "mno"; constexpr char* LOOP_END = (char*) "pqr"; constexpr char* OUTPUT = (char*) "stu"; constexpr char* INPUT = (char*) "vwx"; constexpr int order_len = 3; |
このとき、コンパイル時定数は、型変換を明記しておかないと Warning が出てしまいます。
なので、一応 (char*) のように、キャストしてあげましょう。
また、実装のしやすさを考慮して、命令の長さも固定しておくパターンにします。
mapのキーも文字列へ
1 2 3 4 5 6 7 8 9 10 | map<string , void(*)(unsigned char*, unsigned int&, unsigned int&, string&, stack<int>&, unsigned int&)> bfs = { {INCREMENT, increment}, {DECREMENT, decrement}, {RIGHT, right}, {LEFT, left}, {OUTPUT, output}, {INPUT, input}, {LOOP_START, loop_start}, {LOOP_END, loop_end}, }; |
細かな調整
命令をコードからどう切り取るか、コードポインタをどう進めるかも、修正していきましょう。
1 2 3 4 5 6 7 8 9 | /* Brainfuck 解析 */ while (code_ptr < code_len){ // もしキーが存在すれば string key = code.substr(code_ptr, order_len); // [changed] substr()で命令を抽出 if (bfs.count(key) > 0){ bfs[key](memory, ptr, code_ptr, code, loops, code_len); } code_ptr += order_len; // [changed] } |
あとは、ループ処理の部分で、コードポインタの取り扱いを変更するだけとなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | void loop_start(unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ loops.push(code_ptr); // "["の位置を保存 /* ポインタの値が0ならば "]" に飛ぶ */ if (memory[ptr] == 0) { int depth = 1; //! ループの深さ // 対応する "]" を探す while (depth > 0){ code_ptr += order_len; // [changed] コードポインタを進める // もしコード終端に来てしまったらエラー if (code_ptr >= code_len){ cerr << "Error: Cannot find \"]\"." << endl; exit(-1); } // "[" と "]" を見つけたら深さを修正 if (code.substr(code_ptr, order_len) == LOOP_START) // [changed] depth++; if (code.substr(code_ptr, order_len) == LOOP_END) // [changed] depth--; } // もしdepth=0であれば、対応する "]" が見つかったので抜ける loops.pop(); // ループ終わり } } void loop_end (unsigned char* memory, unsigned int& ptr, unsigned int& code_ptr, string& code, stack<int>& loops, unsigned int& code_len){ if(loops.empty()){ cerr << "Error: Loop start order, " << LOOP_START << ", is not found." <<endl; exit(-1); } code_ptr = loops.top() - order_len; // [changed] 対応する "[" に移動 loops.pop(); // 飛んだらまたスタックされるのでポップしておく } |
これで OK です。
試してみる
実装の簡略化で、コードの空白と改行については、取り除く部分は書いていません。
なので、それらがない Esolang コードを、読み取らせてみましょう!
1 | abcabcabcabcabcabcabcabcabcmnodefghiabcabcabcabcabcabcabcabcghiabcabcabcabcabcabcabcabcabcabcabcghiabcabcabcabcabcjkljkljklpqrghistughiabcabcstuabcabcabcabcabcabcabcstustuabcabcabcstughidefstudefdefdefdefdefdefdefdefdefdefdefdefstujklabcabcabcabcabcabcabcabcstudefdefdefdefdefdefdefdefstuabcabcabcstudefdefdefdefdefdefstudefdefdefdefdefdefdefdefstughiabcstu |
1 2 3 | $ make $ ./brainfuck 3byte_hello/bf Hello, world! |
上のようになれば成功です!
より難解な、オリジナル Esolang が出来上がりましたね!
さいごに
今回は番外編で、オリジナルの Esolang を作成してみました。
もしかしたら、「少し難しい内容だった」という方も、中にいるかもしれませんね!
ただ、もっと改良すれば、日本語のマルチバイト文字を使った Esolang も実現できてしまいます。
余力のある方は、ぜひチャレンジしてみてくださいね!
以上で、番外編含め本連載は終了になります。
ここまでお付き合いいただき、ありがとうございました!
第1回目はこちら!
こちらの記事もオススメ!
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ライトコードの日常12月 1, 2023ライトコードクエスト〜東京オフィス歴史編〜
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン