原文地址:C /Rust 元编程之 BrainFuck 编译器(constexpr/ 过程宏解法)
引子
接上一篇C 元编程之 BrainFuck 编译器(模板元解法)挖了个坑:用constexpr方式实现,我发现更容易实现了,代码不到100行搞定,同时也尝试了一下用Rust过程宏来做元编程,最后我会对这两者进行比较。
之前模板元方式解法不支持嵌套循环,同时也不支持输入输出,在这次实现中,支持嵌套循环、输出。
C 版本:
代码语言:javascript复制// compile time
constexpr auto res = brain_fuck(R"(
[> [> > > > <<<<-]> > >->> [<]<-]>>.
>---. .. .>>.<-.<. .------.--------.>> .> .
)");
puts(res);
// runtime
if (argc > 1) puts(brain_fuck(argv[1]));
Rust版本:
代码语言:javascript复制println!("{}", brain_fuck!(
[> [> > > > <<<<-]> > >->> [<]<-]>>.
>---. .. .>>.<-.<. .------.--------.>> .> .
));
而两者背后实现的算法是一致的。
C constexpr解法
其实模板元解法和constexpr解法能力相同,只是实现代价不同,后者更容易实现,写起来就像普通函数一样。运行结果可看:https://godbolt.org/z/EYn7PG
首先定义一个Stream类,用于存放输出结果:
代码语言:javascript复制template<size_t N>
class Stream {
public:
constexpr void push(char c) { data_[idx_ ] = c; }
constexpr operator const char*() const { return data_; }
constexpr size_t size() { return idx_; }
private:
size_t idx_{};
char data_[N]{};
};
然后写一个parse函数,解析BrainFuck代码,经典的递归下降分析:
代码语言:javascript复制template<typename STREAM>
constexpr auto parse(const char* input, bool skip, char* cells,
size_t& pc, STREAM&& output) -> size_t {
const char* c = input;
while(*c) {
switch(*c) {
case ' ': if (!skip) cells[pc]; break;
case '-': if (!skip) --cells[pc]; break;
case '.': if (!skip) output.push(cells[pc]); break;
case '>': if (!skip) pc; break;
case '<': if (!skip) --pc; break;
case '[': {
while (!skip && cells[pc] != 0)
parse(c 1, false, cells, pc, output);
c = parse(c 1, true, cells, pc, output) 1;
} break;
case ']': return c - input;
default: break;
}
c;
}
return c - input;
}
constexpr size_t CELL_SIZE = 16;
template<typename STREAM>
constexpr auto parse(const char* input, STREAM&& output) -> STREAM&& {
char cells[CELL_SIZE]{};
size_t pc{};
parse(input, false, cells, pc, output);
return output;
}
最后用brain_fuck函数串起来:
代码语言:javascript复制template<size_t OUTPUT_SIZE = 15>
constexpr auto brain_fuck(const char* input) {
Stream<OUTPUT_SIZE> output;
return parse(input, output);
}
以上就是实现一个BrainFuck编译器的所有细节。
延伸一下,如果你细心的话,你会发现输出大小需要手动指定(默认15字节),如果大小过大,那么多余的空间浪费了;如果大小过小,编译报错。思考一下,有什么办法确定大小呢?毕竟C 20之前constexpr不支持动态分配内存,像链表这种随时扩容的方式暂时不可行。
我们可以实现一个函数brain_fuck_output_size来提前计算好所需要大小:
代码语言:javascript复制// calculate output size
constexpr auto brain_fuck_output_size(const char* input) -> size_t {
struct {
size_t sz{};
constexpr void push(...) { sz; }
} dummy;
return parse(input, dummy).sz 1; // include '