9
\$\begingroup\$

I wrote this small Brainfuck interpreter in C++ where the different op codes are handled in one big switch-case statement instead of something like a tokenizer as Brainfuck is very simple in that regard. Furthermore, I split the logic into namespaces as I wanted to emulate or have the behaviour as if it was a static utility method (like static in Java).

My cells array has a size of 30 000 but is technically an infinite tape as I read somewhere that that size is appropriate for Brainfuck. I also differentiate between handling the input and output as ASCII characters or just numbers in case the user wants to just print Hello Word or compute 1+1 but I feel like there must be a better solution. unsigned char is the datatype I store the current values in as it goes from 0 to 255 which is what Brainfuck specifies I believe.

brainfuck.h

#include <string>

namespace Brainfuck {

    void execute(const std::string &input, std::string &output, const char &ascii);

}

brainfuck.cpp

#include "brainfuck.h"

#include <array>
#include <string>
#include <algorithm>
#include <iostream>

namespace Brainfuck {

    namespace {
        std::array<unsigned char, 30'000> cells; 
        std::string input_;
        
        int current_index = 0;
        int current_char = 0;

        void remove_spaces(std::string &input) {
            input.erase(std::remove_if(input.begin(), input.end(), ::isspace), input.end());
        }

        void remove_new_lines(std::string &input) {
            input.erase(std::remove(input.begin(), input.end(), '\n'), input.end());
        }

        int find_matching_end_bracket(int current_char, const std::string &input) {
            int new_index = 0;
            int expected = 0;
            int found = 0;
            for (;current_char < input.length(); current_char++) {
                if(input[current_char] == '[') expected++;
                if(input[current_char] == ']') {
                    found++;
                    if(expected == found) {
                        new_index = current_char;
                        break;
                    }
                }
            }
            return new_index;
        }

        int find_matching_start_bracket(int current_char, const std::string &input) {
            int new_index = 0;
            int expected = 0;
            int found = 0;
            for (;current_char >= 0; current_char--) {
                if(input[current_char] == ']') expected++;
                if(input[current_char] == '[') {
                    found++;
                    if(expected == found) {
                        new_index = current_char;
                        break;
                    }
                }
            }
            return new_index;
        }

        void op_codes(const char &cur, std::string &output, const char &ascii) {
            switch (cur)
            {
                case '>':
                    if(current_index == cells.size() - 1) {
                        current_index = 0;
                    } else {
                        current_index++;
                    }
                    break;
                case '<':
                    if(current_index == 0) {
                        current_index = cells.size() - 1;
                    } else {
                        current_index--;
                    }
                    break;
                case '+':
                    cells[current_index]++;
                    break;
                case '-':
                    cells[current_index]--;
                    break;
                case '.':
                    if(ascii == 'n') {
                        output += std::to_string(cells[current_index]);
                    }
                    if(ascii == 'y') {
                        output += cells[current_index];
                    }
                    break;
                case ',':
                {
                    std::string in;
                    std::cout << "Enter input between 0 and 255: ";
                    std::cin >> in;
                    if(!in.empty() && std::all_of(in.begin(), in.end(), ::isdigit)) {
                        auto c_in = stoi(in);
                        if(c_in >= 0 && c_in <= 255) {
                            cells[current_index] = c_in;
                        } else {
                            std::cout << "Not a valid input! Enter a number between 0 and 255!" << std::endl;
                        }
                    } else {
                        std::cout << "Not a valid input!" << std::endl;
                    }
                    break;
                }
                case '[':
                    if(cells[current_index] == 0) {
                        current_char = find_matching_end_bracket(current_char, input_);
                    }
                    break;
                case ']':
                    if(cells[current_index] != 0) {
                        current_char = find_matching_start_bracket(current_char, input_);
                    }
                    break;
                default:
                    std::cout << "Not a valid Brainfuck program! Unknown symbol at " << current_index << std::endl;
                    break;
            }
        }


        void reset() {
            std::fill(cells.begin(), cells.end(), 0);
            current_char = 0;
            current_index = 0;
            input_ = "";
        }
    }

    void execute(const std::string &input, std::string &output, const char &ascii) {
        input_ = input;
        remove_spaces(input_);
        remove_new_lines(input_);
        
        while(current_char < input_.length()) {
            op_codes(input_[current_char], output, ascii);
            current_char++;
        }

        reset();
    }
}

main.cpp

#include "brainfuck.h"

#include <iostream>
#include <string>

int main(int, char**) {

    std::string brainfuck_code;
    std::string output;
    char ascii;

    std::cout << "Enter your brainfuck code: ";
    std::cin >> brainfuck_code;
    do {
        std::cout << "Do you wish your output to be letters? y/n ";
        std::cin >> ascii;
    } while(ascii != 'y' && ascii != 'n');
    
    Brainfuck::execute(brainfuck_code, output, ascii);

    std::cout << "\n";
    std::cout << output << std::endl;
    
    return EXIT_SUCCESS;
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ as it goes from 0 to 255 - That's true on most modern machines (C++ implementations), but ISO C++ in general allows unsigned char to be larger (not smaller). Use uint8_t if that's exactly what you need. \$\endgroup\$ – Peter Cordes Feb 8 at 7:09
  • \$\begingroup\$ You know, you can translate Brainfuck to C quite easily, right? \$\endgroup\$ – Oleg Lobachev Feb 8 at 12:21
  • \$\begingroup\$ It is such a ridiculous and pointless name for a psuedo-language. \$\endgroup\$ – Fattie Feb 8 at 16:52
  • \$\begingroup\$ @PeterCordes thanks for the hint, I will change that! \$\endgroup\$ – xkevio Feb 9 at 1:03
  • 1
    \$\begingroup\$ If std::uint8_t exists, then unsigned char must be 0 to 255 (and std::uint8_t probably is unsigned char). If unsigned char is not 0 to 255, then std::uint8_t can’t exist. If you want to be truly portable, you’d have to use std::uint_fast8_t or std::uint_least8_t… or unsigned char. (Or, maybe, std::byte.) uint8_t is actually the least portable option. \$\endgroup\$ – indi Feb 11 at 1:34
9
\$\begingroup\$
std::array<unsigned char, 30'000> cells; 
int current_index = 0;

case '<':
    if(current_index == 0) {
        current_index = cells.size() - 1;
    } else {
        current_index--;
    }
    break;

Try using a truly infinite tape instead. (Well, up to the limits of your memory allocator, anyway.) Here's what part of that would look like. Can you fill in the rest?

std::deque<unsigned char> cells; 
int current_index = 0;

case '<':
    if (current_index == 0) {
        cells.push_front(0);
    } else {
        current_index -= 1;
    }
    break;

I split the logic into namespaces as I wanted to emulate or have the behaviour as if it was a static utility method

You know C++ has the static keyword too, right?

class Brainfuck {
public:
    static void execute(const std::string &input, std::string &output, bool ascii);
};

(I'm not sure why you were taking the ascii parameter as const char& — it's just a boolean true or false, isn't it?)


Rather than using a bunch of global variables, consider making each instance of class Brainfuck represent an individual program, which you can execute by calling the execute method. So then you can juggle multiple Brainfuck programs within a single C++ program.


You currently handle output by modifying a std::string& output. It would be more "C++-thonic" to use the standard library's <iostream> facilities, like this:

class Brainfuck {
public:
    explicit Brainfuck(const std::string& program);
    void execute(std::istream& input, std::ostream& output, bool outputAscii) const {
        ~~~
        case '.':
            if (this->outputAscii) {
                output << char(cells[current_index]);
            } else {
                output << int(cells[current_index]) << ' ';
            }
        ~~~
    }
};

You could even define your own "output formatter" class, something like

class Brainfuck {
public:
    explicit Brainfuck(const std::string& program);

    template<class Outputter>
    void execute(std::istream& input, const Outputter& output) const {
        ~~~
        case '.':
            output(cells[current_index]);
        ~~~
    }
};

which would be called like

Brainfuck bf("--[+++++++<---->>-->+>+>+<<<<]<.>++++[-<++++>>->--<<]>>-.>--..>+.<<<.<<-.>>+>->>.+++[.<]");
bf.execute(std::cin, [&](char ch) {
    std::cout << ch;  // Hello world!
});
bf.execute(std::cin, [&](char ch) {
    std::cout << int(ch) << ' ';  // 72 101 108 ...
});
\$\endgroup\$
3
  • \$\begingroup\$ Try using a truly infinite tape instead. (Well, up to the limits of your memory allocator, anyway.) Good idea, I agree I should use that as I am aware of that particular data structure and how it works. I know that C++ also has the static keyword but when I researched how one would implement such utility classes, lots of people suggested using namespaces instead of static functions in a separate class. (ascii is a char here just so I can check if the user actually typed in y or n which is of course not optimal and rather lazy. \$\endgroup\$ – xkevio Feb 8 at 0:21
  • \$\begingroup\$ Rather than using a bunch of global variables, consider making each instance of class Brainfuck represent an individual program, which you can execute by calling the execute method. So then you can juggle multiple Brainfuck programs within a single C++ program. That is what I was wondering when I researched how to best go about this design pattern wise, if a class is smart so each program is separate or the aforementioned namespaces. I will look into the iostream way you suggested, thank you. \$\endgroup\$ – xkevio Feb 8 at 0:22
  • \$\begingroup\$ "ascii is a char here just so I can check if the user actually typed in y or n which is of course not optimal" — Right. This and the iostreams thing are both examples of the "Separation of Responsibilities" or "Single Responsibility Principle." class Brainfuck is for executing/manipulating BF programs; don't make it also responsible for parsing a user-input y/n into a boolean! Write a separate argument-parsing function bool yesno_to_bool(char ch) for that, if you need it (but honestly it seems like you don't). \$\endgroup\$ – Quuxplusone Feb 8 at 14:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.