• トップ
  • ブログ一覧
  • 【第3回】Brainfuckを実装しながら学ぶC++【実装してみよう!中編】
  • 【第3回】Brainfuckを実装しながら学ぶC++【実装してみよう!中編】

    広告メディア事業部広告メディア事業部
    2020.12.22

    IT技術

    第3回~Brainfuckを実装しながら学ぶC++

    今回で、「Brainfuckを実装しながら学ぶC++」シリーズの第3回目となりました。

    ちなみに前回は、今回に備えて、C/C++ の基礎を解説していきましたね!

    【第2回】Brainfuckを実装しながら学ぶC++【実装してみよう!前編】2020.12.15【第2回】Brainfuckを実装しながら学ぶC++【実装してみよう!前編】第2回~Brainfuckを実装しながら学ぶC++連載「Brainfuckを実装しながら学ぶC++」シリーズの第2回目...

    さて今回は、いよいよ本格的な 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 の使用を推奨しています。

    各種変数の初期化

    次に、以下を初期化するコードを書いていきましょう。

    1. メモリのサイズ
    2. ポインタの初期位置や値
    3. Brainfuck コード読み取りのポインタ
    4. 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回はこちら!

    【第4回】Brainfuckを実装しながら学ぶC++【実装してみよう!後編】2020.12.22【第4回】Brainfuckを実装しながら学ぶC++【実装してみよう!後編】第4回~Brainfuckを実装しながら学ぶC++「Brainfuckを実装しながら学ぶC++」シリーズも、今回を含め...

    第1回目はこちら!

    brainfuckを実装しながら学ぶC++2020.12.08【第1回】Brainfuckを実装しながら学ぶC++【Brainfuckとは】第1回目~Brainfuckを実装しながらC++学ぼう本連載では、「Brainfuck」の特性を理解しながら、実際に ...

    こちらの記事もオススメ!

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
    featureImg2020.07.27IT・コンピューターの歴史特集IT・コンピューターの歴史をまとめていきたいと思います!弊社ブログにある記事のみで構成しているため、まだ「未完成状態」...

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    GitHubActionsのランナーに触れてみた

    こやまん(エンジニア)

    こやまん(エンジニア)

    2024.03.28

    IT技術

    Azure Data FactoryでSlackへ通知をしてみる

    たかやん(エンジニア)

    たかやん(エンジニア)

    2024.03.28

    IT技術

    GCP Secret Managerを使ってみた

    たなゆー(エンジニア)

    たなゆー(エンジニア)

    2024.03.21

    IT技術

    Bitriseのパイプラインと環境変数

    加納(エンジニア)

    加納(エンジニア)

    2024.03.11

    IT技術