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;
}
1 Answer 1
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 ...
});
-
\$\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 achar
here just so I can check if the user actually typed iny
orn
which is of course not optimal and rather lazy. \$\endgroup\$xkevio– xkevio2021年02月08日 00:21:51 +00:00Commented 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 theiostream
way you suggested, thank you. \$\endgroup\$xkevio– xkevio2021年02月08日 00:22:13 +00:00Commented 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-inputy/n
into a boolean! Write a separate argument-parsing functionbool yesno_to_bool(char ch)
for that, if you need it (but honestly it seems like you don't). \$\endgroup\$Quuxplusone– Quuxplusone2021年02月08日 14:36:20 +00:00Commented Feb 8, 2021 at 14:36
unsigned char
to be larger (not smaller). Useuint8_t
if that's exactly what you need. \$\endgroup\$std::uint8_t
exists, thenunsigned char
must be 0 to 255 (andstd::uint8_t
probably isunsigned char
). Ifunsigned char
is not 0 to 255, thenstd::uint8_t
can’t exist. If you want to be truly portable, you’d have to usestd::uint_fast8_t
orstd::uint_least8_t
... orunsigned char
. (Or, maybe,std::byte
.)uint8_t
is actually the least portable option. \$\endgroup\$