我知道有很多,但是这不一样。为什么?因为它可以实时评估脑力激荡的代码,从输入文件中读取代码,并在无法访问时消除代码,而不是读取整个程序,对其进行分析和评估。
我在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;
}
#1 楼
看起来很棒!只是一些(小的)建议:样式
它确实是非常易读和直接的,但是突破一些功能当然不会受到伤害。 br />即使您没有使用
using namespace std;
(呼!),变量名称stack
仍然让我有点不舒服(然后,我再也想不出它的名字了,因为它不会变得很粗糙)。 如果您最终将事情拖入功能中,则可以将程序定向在模板化的迭代器周围(使用istream_iterator来完成您现在正在做的事情)。这样您就可以流式传输程序或将其预先存储。我想这并不能真正完成很多事情,但是我认为值得一提的是,如果不这样做就将其分解成较小的功能会很快变得很混乱。
是
STACK_INITIAL_SIZE
常量而不是宏。超级主观的东西,但是按字母顺序包括的内容在脑海中扫描起来会更容易一些。 br />
DEAD_CODE_LIMIT
从技术上讲应该是const int
,尽管实际上并不重要。您错过了
size_t
的包含内容。std::size_t
和cstdio
应该是getchar
和putchar
。设计
这也许是我在您的代码中看到的最刺眼的东西:
std::getchar
应该是std::putchar
。实际上,它几乎已经是一个手动管理的向量。只要您保持谨慎,性能就会保持不变,而且,值得庆幸的是,应该放弃大量代码(调整大小,清零等)。手动内存(和资源)管理应该非常少见(它应该仅限于类似库的容器,即使在那里也是如此)。对于资源,总有RAII方法,对于内存,有
stack
和std::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口译员进入自己的班级。然后,考虑起来就容易得多了(可以将初始化放在一个位置(构造函数))。析构函数可以保持整洁等。
评论
我将BF解释器封装到其自己的类中。 codereview.stackexchange.com/q/84393/507