7
\$\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;
}
asked Feb 7, 2021 at 21:56
\$\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\$ Commented Feb 8, 2021 at 7:09
  • \$\begingroup\$ You know, you can translate Brainfuck to C quite easily, right? \$\endgroup\$ Commented Feb 8, 2021 at 12:21
  • \$\begingroup\$ It is such a ridiculous and pointless name for a psuedo-language. \$\endgroup\$ Commented Feb 8, 2021 at 16:52
  • \$\begingroup\$ @PeterCordes thanks for the hint, I will change that! \$\endgroup\$ Commented Feb 9, 2021 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\$ Commented Feb 11, 2021 at 1:34

1 Answer 1

8
\$\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 ...
});
answered Feb 7, 2021 at 23:16
\$\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\$ Commented Feb 8, 2021 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\$ Commented Feb 8, 2021 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\$ Commented Feb 8, 2021 at 14:36

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.