【第3回】Brainfuckを実装しながら学ぶC++【実装してみよう!中編】
IT技術
今回で、「Brainfuckを実装しながら学ぶC++」シリーズの第3回目となりました。
ちなみに前回は、今回に備えて、C/C++ の基礎を解説していきましたね!
さて今回は、いよいよ本格的な BrainFuck の実装に入っていきます。
やや長丁場になりますが、一緒に頑張っていきましょう!
大枠の実装
前回に引き続き、「brainfuck」プロジェクトを使用します。
まずは、大枠を作っていきましょう!
命令の定義
連載の第1回目に解説したように、Brainfuck には、8つの命令が存在します。
それらを定数として、まずは定義していきましょう。
1#include <iostream>
2using namespace std;
3
4// 命令をコンパイル時定数として定義
5constexpr char INCREMENT = '+';
6constexpr char DECREMENT = '-';
7constexpr char RIGHT = '>';
8constexpr char LEFT = '<';
9constexpr char LOOP_START = '[';
10constexpr char LOOP_END = ']';
11constexpr char OUTPUT = '.';
12constexpr char INPUT = ',';
13
14int main() {
15
16 return 0;
17}
C++ 11 からサポートされる constexpr 修飾子は、以前まで使われていた #define INCREMENT '+' という、マクロ定義によるコンパイル時定数と同じ役割。
これらはその名の通り、コンパイル時に評価され展開されるため、実行時間の短縮につながります。
ちなみに C++11 以降でも、#define で定数定義している人は多くいますが、公式では constexpr の使用を推奨しています。
各種変数の初期化
次に、以下を初期化するコードを書いていきましょう。
- メモリのサイズ
- ポインタの初期位置や値
- Brainfuck コード読み取りのポインタ
- Brainfuck コードの長さ
実際のコードは、次の通り。
1int main() {
2
3 unsigned char memory[MEMORY_SIZE]; //! 1Byte=8bit (0~255) で構成されるメモリを定義
4 unsigned int ptr = 0; //! メモリポインタ (負の値は取らないのでunsigned)
5 unsigned int code_ptr = 0; //! Brainfuckコードのポインタ
6 unsigned int code_len = 0; //! Brainfuckコードの終端 (長さ)
7
8 // メモリの0初期化
9 memset(memory, 0, sizeof(memory));
10
11
12 return 0;
13}
memset() は、C 言語時代からあるもので、配列要素を一括初期化できる関数です。
ただ現在では、今回のような単純な配列の初期化などに、使われることが多くなりました。
ファイルの読み込み
今回作成しているインタプリタは、以下のような使い方を想定しています。
1$ brainfuck nanikashirano_code.bf
ですので、「コマンドラインからの引数受け取り処理」と「そのファイル読み込み機構」を、ここで実装していきたいと思います。
コマンドライン引数の受け取り
C++ では、標準でコマンドライン引数を処理する機能があるので、とても簡単!
メイン関数に、以下のような引数を指定します。
1int main(int argc, char* argv[]) {
2 // ...
3}
argc は、自身の命令と、後続のコマンドライン引数を含んだ数値が格納されます。
argv 配列には、それらの文字列が格納されます。
例えば、$ brainfuck samplecode.bf と実行すれば、
1> argc = 2
2> argv[0] = "brainfuck"
3> argv[1] = "samplecode.bf"
となります。
念のため、引数がなかった場合のエラー処理も書いておきましょう!
1int main(int argc, char* argv[]) {
2
3 /* 省略 */
4
5 /* コマンドライン引数処理 */
6 if (argc < 2){
7 cerr << "Error: A Brainfuck code is not passed as a command-line argument." << endl;
8 cerr << "Please pass an argument as the form, $ brainfuck [FILENAME]." << endl;
9 return -1;
10 }
11}
cerr は、エラー出力用の出力ストリームです。
ファイル読み込み
次に、コマンドライン引数で受け取ったファイル名を元に、ファイルを開く処理を実装しましょう。
ファイルの扱いは、#include <fstream> で使用可能になります。
ファイル読み込みは std::ifstream、書き込みは std::ofstream を使用します。
今回は、読み込みだけなので、以下のようになります。
1#include <iostream>
2#include <fstream> // 追加
3using namespace std;
4
5/* 省略 */
6
7int main(int argc, char* argv[]) {
8
9 /* 省略 */
10
11 ifstream file(argv[1]); //! ファイル読み込み
12 if (!file){ // ファイル読みこみ失敗
13 cerr << "Error: The file, " << argv[1] << ", cannot be opened." << endl;
14 return -1;
15 }
16
17 return 0;
18}
ファイル処理
それでは、ファイルが上手く開けた場合の処理を考えます。
ここで取得したいのは、ファイルの中身 (スペース・改行含む) と、コードの長さ。
ファイル全体を一括で読み込みたいときは、std::stringstream を利用することで、簡単に実現できます。
1#include <iostream>
2#include <fstream>
3#include <string> //追加
4#include <sstream> // 追加
5using namespace std;
6
7/* 省略 */
8
9int main(int argc, char* argv[]) {
10
11 /* 省略 */
12
13 /* ファイルの中身を取得 */
14 stringstream buffer; //! stringstream用のバッファ
15 buffer << file.rdbuf(); // ファイルをバッファとして取得
16 string code(buffer.str()); //! codeをstringとして取得
17 code_len = code.size(); //! コードサイズ
18
19 // デバッグ用
20 cout << code_len << " Byte.\n" << code << endl;
21
22
23 return 0;
24}
ここでビルドディレクトリに、以下のような Brainfuck コードを追加して、動作確認をしましょう。
1+++++++++[->++++++++>+++++++++++>+++++<<<]>.>++.
2+++++++..+++.>-.------------.<++++++++.--------.
3+++.------.--------.>+.
上のコードを、「hello.bf」として保存します。
そして、
1$ cd build
2$ make
3$ ./brainfuck hello.bf
4121 Byte.
5+++++++++[->++++++++>+++++++++++>+++++<<<]>.>++.
6+++++++..+++.>-.------------.<++++++++.--------.
7+++.------.--------.>+.
このように表示されれば、ファイル読み込みが上手く動作しています。
コード解析部の大枠作成
次に、核となる Brainfuck コード解析部分の大枠を作成して、今回は終わりにします。
基本は、命令を一つずつ読み込んでいくだけなので、以下のようなループが大枠です。
1/* Brainfuck 解析 */
2while (code_ptr < code_len){
3
4 code_ptr++;
5}
この while 文の中身で、その命令によって処理を分けていくのですが、ここではそれを switch 文を使用して実装していきます。
1/* Brainfuck 解析 */
2while (code_ptr < code_len){
3 switch (code[code_ptr]) {
4 case INCREMENT:
5 break;
6 case DECREMENT:
7 break;
8 case RIGHT:
9 break;
10 case LEFT:
11 break;
12 case LOOP_START:
13 break;
14 case LOOP_END:
15 break;
16 case OUTPUT:
17 break;
18 case INPUT:
19 break;
20 default: // 上記以外の命令はコメント扱い
21 break;
22 }
23 code_ptr++;
24}
まだ、各ケースは実装していないので何も起こりませんが、次回からはこの部分を実装していきます。
ここが完成すれば、Brainfuck インタプリタは出来上がりです!
第4回目へつづく!
今回は、インタプリタ実装に本格的に着手しました。
ただ、まだ「読み込んだコードをそのまま出力するだけ」という、実用性のないコードになっていますね…。
というわけで次回は、これを基に、どんどんコードを発展させていきましょう!
具体的には、インタプリタの核を実装していきます。
では、次回もお楽しみに!
第4回はこちら!
第1回目はこちら!
こちらの記事もオススメ!
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
2020.07.27IT・コンピューターの歴史特集IT・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit