C++/Rust 元編程之 BrainFuck 編譯器(constexpr/ 過程宏解法)

原文地址: C++/Rust 元編程之 BrainFuck 編譯器(constexpr/ 過程宏解法)

引子

接上一篇 C++ 元編程之 BrainFuck 編譯器(模板元解法) 挖了個坑:用constexpr方式實現,我發現更容易實現了,代碼不到100行搞定,同時也嘗試了一下用Rust過程宏來作元編程,最後我會對這二者進行比較。
以前模板元方式解法不支持嵌套循環,同時也不支持輸入輸出,在此次實現中,支持嵌套循環、輸出。
C++版本:
   
// compile time
constexpr auto res = brain_fuck(R"(
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.
>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
)"
);
puts(res);

// runtime
if (argc > 1) puts(brain_fuck(argv[1]));
Rust版本:
   
println!("{}", brain_fuck!(
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.
>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
));
而二者背後實現的算法是一致的。

C++ constexpr解法

其實模板元解法和constexpr解法能力相同,只是實現代價不一樣,後者更容易實現,寫起來就像普通函數同樣。運行結果可看: https://godbolt.org/z/EYn7PG
首先定義一個Stream類,用於存放輸出結果:
   
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代碼,經典的遞歸降低分析:
   
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函數串起來:
   
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來提早計算好所須要大小:
   
// 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 '\0'
}
咱們實現一個dummy對象,其push接口只是簡單地計數,最終dummy的長度就是輸出的長度。
這也是爲啥STREAM做爲模板參數類型的緣由,由於只須要依賴 push 接口,而不須要依賴具體的類型,這也是泛型的魅力。
   
#define BRAIN_FUCK(in) brain_fuck<brain_fuck_output_size(in)>(in)
constexpr auto res = BRAIN_FUCK(R"(
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.
>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.
)"
);

Rust過程宏解法

Rust作元編程,目前只能經過宏的方式作,並且能力也有限。這裏須要用過程宏手段。目前我把 brain_fuck 提交到cargo倉庫了: https://crates.io/crates/brain_fuck ,能夠體驗一下。
一樣地寫一個parse函數:
   
const CELL_SIZE: usize = 16;
fn parse(code: &[u8], skip: bool, cells: &mut [u8; CELL_SIZE],
pc: &mut usize, output: &mut Vec<u8>) -> usize {
let mut idx = 0;
while idx < code.len() {
let c = code[idx];
match c {
b'+' if !skip => cells[*pc] += 1,
b'-' if !skip => cells[*pc] -= 1,
b'.' if !skip => output.push(cells[*pc]),
b'>' if !skip => *pc += 1,
b'<' if !skip => *pc -= 1,
b'[' => {
while !skip && cells[*pc] != 0 {
parse(&code[idx+1..], false, cells, pc, output);
}
idx += parse(&code[idx+1..], true, cells, pc, output) + 1;
},
b']' => return idx,
_ => {}
}
idx += 1;
}
idx
}
用過程宏brain_fuck包裝一下:
   
#[proc_macro]
pub fn brain_fuck(_item: TokenStream) -> TokenStream {
let input = _item.to_string();
let mut cells: [u8; CELL_SIZE] = [0; CELL_SIZE];
let mut pc = 0;
let mut output = Vec::<u8>::new();

parse(&input.as_bytes(), false, &mut cells, &mut pc, &mut output);

TokenStream::from_str(
&format!("\"{}\"", from_utf8(&output).unwrap())
).unwrap()
}

結論

對比C++和Rust版本,實現代價同樣。
C++版本實現過程當中能夠先不加constexpr關鍵字,經過打印等debug手段調試經過後,最終加上constexpr關鍵字便可,最後既能夠在運行時使用,也能夠在編譯時使用。若是在編譯期出現內存越界(cells越界)狀況下,編譯報錯,即避免了ub。
Rust實現過程宏只能經過lib方式作,一樣地也能夠直接加打印,在編譯的時候輸出,最終將打印去掉。輸出結果能夠直接用 Vec 這種動態容器存,C++20以前暫時得經過定長(預留長度或提早計算)數組搞。而Rust的過程宏只能用在編譯時,沒法用在運行時,並且只支持字面量方式,不支持變量傳參給過程宏。
從生成的彙編結果來看,C++版本更加簡單粗暴,g++編譯器生成的彙編字符串結果直接存到8字節整型中,clang則比較直觀,main和數據只有15行:
   
main: # @main
subq $24, %rsp
movq .L__const.main.res+16(%rip), %rax
movq %rax, 16(%rsp)
movups .L__const.main.res(%rip), %xmm0
movaps %xmm0, (%rsp)
leaq 8(%rsp), %rdi
callq puts
xorl %eax, %eax
addq $24, %rsp
retq
.L__const.main.res:
.quad 13 # 0xd
.asciz "Hello World!\n\000"
.zero 1
而Rust編譯器生成的彙編結果就不夠C++那麼簡潔緊湊,這裏就不貼出來了。

本文分享自微信公衆號 - Rust語言中文社區(rust-china)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。算法

相關文章
相關標籤/搜索