昨天早上我很无聊,所以我写了一个脑筋急转弯的翻译。

我知道有很多,但是这不一样。为什么?因为它可以实时评估脑力激荡的代码,从输入文件中读取代码,并在无法访问时消除代码,而不是读取整个程序,对其进行分析和评估。

我在C ++,因为这样一来,我不必重新编写向量,堆栈...

我知道一些不良的程序设计实践:


它是单片的,尽管我觉得它可读性强……不过它只是个脑力劳动的解释器,因此与其使用所有程序数据并对其进行拆分的上下文结构,我发现继续主要功能更加容易。 />在评估中使用C函数(getchar / putchar)而不是使用C ++的流。我认为这种方式更具可读性。

我已经在c ++ 11模式下使用g ++对其进行了编译:

$ g++ -std=c++11 -Wall bf.cc -o bf

要运行一个程序:

$ ./bf program.bf

我已经成功测试了此处的所有文件。

我关注的是:


性能:我使它比那里的大多数口译员更快。是否有任何性能暗示可以使其更快地运行?
可读性:我认为,即使它是整体的,它也具有足够的可读性。是真的吗?
我可能会错过任何bug /内存泄漏。

代码

#include <iostream>
#include <vector>
#include <stack>
#include <fstream>

#define STACK_INITIAL_SIZE 300
#define DEAD_CODE_LIMIT 100
typedef unsigned char byte;

enum class State {
    READING,
    WHILE_IGNORING
};

int main(int argc, char** argv) {

    if ( argc < 2 ) {
        std::cerr << "Usage: " << argv[0] << " <file_name>" << std::endl;
        exit(1);
    }

    /** Get the source stream. */
    std::ifstream source(argv[1]);

    if ( ! source ) {
        std::cerr << "Invalid file: " << argv[1] << std::endl;
        exit(1);
    }

    /** Allocate the program stack. */
    size_t stack_size = STACK_INITIAL_SIZE;
    byte* stack = new byte[stack_size];

    /** Ensure it's initialized to zero */
    std::fill(stack, stack + stack_size, 0);

    /** Initial offset: pointing to the beginning of the stack */
    size_t offset = 0;

    /** Store the tokens, to allow looping */
    std::vector<char> tokens;
    size_t current_token_index = 0;

    /** Store the loop entry points */
    std::stack<size_t> entry_points;

    /** Store the states, to allow multiple looping */
    std::stack<State> states;

    /** Start reading */
    states.push(State::READING);

    while ( true ) {
        /** Declare the token */
        char token;

        /** Get the current state */
        State state = states.top();

        /** Realloc if we have not enough space, allocate 2 * stack_size */
        if ( offset == stack_size ) {
            size_t new_size = 2 * stack_size;

            /** Allocate space */
            byte* tmp = new byte[new_size];

            /** Copy old data */
            std::copy(stack, stack + stack_size, tmp);

            /** Set to 0 new data */
            std::fill(tmp + stack_size, tmp + new_size, 0);

            /** Delete old space */
            delete[] stack;

            /** Set the new stack data */
            stack = tmp;

            /** Keep track of the new stack size */
            stack_size = new_size;
        }

        /** If we are reading from `tokens` and reached the end, read next token from the file and push it into `tokens` */
        if ( current_token_index == tokens.size() ) {
            if ( (source >> token) )
                tokens.push_back(token);
            else
                break; /** Exit if the program ended */
        } else {
            token = tokens[current_token_index];
        }

        /** If we are ignoring chars... Just process '[' (add to the state stack) and ']' (remove from the state stack) */
        if ( state == State::WHILE_IGNORING && ! (token == ']' || token == '[') ) {
            current_token_index++;
            continue;
        }

        /** Main processing */
        switch ( token ) {
            case '>':
                offset++;
                break;
            case '<':
                offset--;
                break;
            case '+':
                stack[offset]++;
                break;
            case '-':
                stack[offset]--;
                break;
            /**
             * I know these could be written as
             * std::cout << static_cast<char>(stack[offset]);
             * and
             * std::cin >> static_cast<char>(stack[offset]);
             * but i find this way more readable
             */
            case '.':
                putchar(stack[offset]);
                fflush(stdout);
                break;
            case ',':
                stack[offset] = getchar();
                fflush(stdin);
                break;
            case '[':
                /** Add the current token to the stack, to come back later */
                entry_points.push(current_token_index);

                /** If the condition is false, or we're already ignoring, just ignore */
                if ( state == State::WHILE_IGNORING || ! stack[offset] )
                    states.push(State::WHILE_IGNORING);
                break;
            case ']':
                /** If we're ignoring just remove the last state */
                if ( state == State::WHILE_IGNORING )
                    states.pop();
                /** Else go back to the loop */
                else
                    current_token_index = entry_points.top() - 1;

                /** Remove the last entry_point */
                entry_points.pop();
                break;
            default:
                break; // ignore comments
        }

        /** Go to the next token */
        current_token_index++;

        /** Dead code elimination */
        if ( current_token_index > DEAD_CODE_LIMIT && entry_points.empty() ) {
            tokens.clear();
            current_token_index = 0;
        }
    }

    /** Program terminated, delete the stack data */
    delete[] stack;

    return 0;
}


评论

我将BF解释器封装到其自己的类中。 codereview.stackexchange.com/q/84393/507

#1 楼

看起来很棒!只是一些(小的)建议:

样式


它确实是非常易读和直接的,但是突破一些功能当然不会受到伤害。 br />即使您没有使用using namespace std;(呼!),变量名称stack仍然让我有点不舒服(然后,我再也想不出它的名字了,因为它不会变得很粗糙)。
如果您最终将事情拖入功能中,则可以将程序定向在模板化的迭代器周围(使用istream_iterator来完成您现在正在做的事情)。这样您就可以流式传输程序或将其预先存储。我想这并不能真正完成很多事情,但是我认为值得一提的是,如果不这样做就将其分解成较小的功能会很快变得很混乱。
STACK_INITIAL_SIZE常量而不是宏。
超级主观的东西,但是按字母顺序包括的内容在脑海中扫描起来会更容易一些。 br /> DEAD_CODE_LIMIT从技术上讲应该是const int,尽管实际上并不重要。
您错过了size_t的包含内容。

std::size_tcstdio应该是getcharputchar

设计

这也许是我在您的代码中看到的最刺眼的东西:std::getchar应该是std::putchar。实际上,它几乎已经是一个手动管理的向量。只要您保持谨慎,性能就会保持不变,而且,值得庆幸的是,应该放弃大量代码(调整大小,清零等)。手动内存(和资源)管理应该非常少见(它应该仅限于类似库的容器,即使在那里也是如此)。

对于资源,总有RAII方法,对于内存,有stackstd::vector(例如,您的代码可能使用std::shared_ptr)。

为了简化它,在最小和最简单的代码片段中,很难在所有方面都正确地实现内存管理(或更抽象的资源管理)。幸运的是,班级提供了一个不错的,孤立的地方,可以消除并发症并更容易地对行为进行推理。

评论


\ $ \ begingroup \ $
问,因为我不知道。您如何假设它是没有标签的C ++ 11? (您提到了shared_ptr,..)
\ $ \ endgroup \ $
–bhathiya-perera
15年3月18日在3:24

\ $ \ begingroup \ $
@JaDogg有作用域的枚举是死掉的:)
\ $ \ endgroup \ $
– Corbin
2015年3月18日,下午3:45

\ $ \ begingroup \ $
嗨!真的,谢谢您的详细回答。 std :: size_t确实使我措手不及...;)您对std :: vector而不是手动内存管理绝对是正确的。我选择手动是因为我不了解std :: vector :: resize(n,val);。答案非常好,我会等待一天,如果那时没有更好的答案(我真的不这么认为),我会将其标记为已接受。
\ $ \ endgroup \ $
–埃米利奥·科沃斯·阿尔瓦雷斯
15年3月18日在8:09

\ $ \ begingroup \ $
堆栈应为(gasp)std :: stack。嘿,presto,无限的堆栈大小!
\ $ \ endgroup \ $
–子弹头磁铁
15年3月18日在14:36

\ $ \ begingroup \ $
@Bulletmagnet然后,当偏移减少时,您将无法保持状态
\ $ \ endgroup \ $
–埃米利奥·科沃斯·阿尔瓦雷斯
2015年3月18日14:43在

#2 楼


DEAD_CODE_LIMIT是不必要的。解释命令后,除非有活动的[,否则您绝对可以忘记该命令。 (例如,如果程序是+++++[<->],则不需要存储任何前一个+++++。一旦您解释了顶级]并跳过了它,就可以安全地忘记所有前面的标记。 br />至少应特殊使用[-]作为“将当前单元格设置为零”的著名习语。否则,您会浪费可用于实际工作的周期。
对于内存管理,请考虑使用realloc或更高级别的结构(例如std::string::erase),而不是不断使用new + std::copy复制数据。
跳到下一个]时,最好不要注意+-<>字符;但是,您仍需在每次迭代中检查offset == stack_size。编写一些特殊情况的代码以跳到下一个]。通常,使用更多的小函数以简化您的主函数(您可以声明辅助函数inline以获得相同的效率作为内联代码。)
我可能不会完全理解您的代码,但是在给定<<<<<<+时不会崩溃吗? Brainfuck磁带不是“堆栈”。它是双向磁带,需要双向“增长” [1]。您可以通过检查offset < 0并在这种情况下重新分配(并增加offset)来解决此问题。不需要朝任一方向增长,您可以只使用普通的旧数组。)

评论


\ $ \ begingroup \ $
我了解您对DEAD_CODE_LIMIT的看法。应该说这是防止大量释放的一种方法(当您调用std :: vector :: clear();时可能发生)。关于特殊的指令优化...在静态分析代码时,可能很容易做到,但在动态评估代码时却不容易。
\ $ \ endgroup \ $
–埃米利奥·科沃斯·阿尔瓦雷斯
2015年3月18日在8:11



\ $ \ begingroup \ $
当它跳到下一个]时,确实会进行检查。我并没有给予太多的重视,那里的重点。您可以提供有关磁带的资料吗?至少按照Esolangs的说法:“有一个指针,最初指向第一个存储单元。”
\ $ \ endgroup \ $
–埃米利奥·科沃斯·阿尔瓦雷斯
15年3月18日在8:20

\ $ \ begingroup \ $
嗯,我一直认为磁带应该是无限的或循环的,但是muppetlabs.com/~breadbox/bf/standards.html说:“如果程序试图将指针移到第一个数组单元下方,或超出最后一个数组单元格的位置,则该程序的行为是不确定的(一些实现导致指针回绕,但许多(也许是大多数)实现的行为方式与C指针徘徊在任意内存中一致。)” ,如果您的C ++程序在任何输入(甚至是恶意制作的输入)上都有未定义的行为,则它不是一个好的C ++程序。
\ $ \ endgroup \ $
– Quuxplusone
15年3月19日在6:42

\ $ \ begingroup \ $
输入什么意思?我的意思是...如果您向左移动,则不确定,但是...
\ $ \ endgroup \ $
–埃米利奥·科沃斯·阿尔瓦雷斯
15年3月19日在10:11

\ $ \ begingroup \ $
对;您在输入<+上有未定义的行为。因此,问题“是否存在未定义其行为的任何输入?”的答案?是是的。”如果有任何您的行为未定义的输入,那是一件坏事。等效地(在这种情况下):如果您的行为对于任何输入都未定义,则是一件坏事。是的,英语不如数学符号精确。不幸的是,这就是我们必须使用的。
\ $ \ endgroup \ $
– Quuxplusone
15年3月19日在19:31

#3 楼

讨厌您的评论:
    /** Declare the token */
    char token;

    /** Get the current state */
    State state = states.top();

其中90%没有用。
无用评论是不好的。如果它们与代码不同步,则将导致维护负担。这很昂贵。
不要写注释来解释代码(我可以阅读代码并明白其含义)。编写注释,解释为什么要执行某项操作或在较高级别上执行该算法。如果使用好的变量和函数名,则几乎不需要注释(自记录代码)。
您无需手动进行内存管理:
byte* stack = new byte[stack_size];

使用标准库std::vector中的某些内容或创建自己的课程。但是您的业务逻辑和资源管理代码应该完全分开。请参阅:关注点分离。
这种方式也不是异常安全的(在较大的程序中这很重要)。
封装是您的朋友。
我把BF口译员进入自己的班级。然后,考虑起来就容易得多了(可以将初始化放在一个位置(构造函数))。析构函数可以保持整洁等。