C++中如何用结构体表示编程语言解析器的嵌套令牌块
C++中如何用结构体表示编程语言解析器的嵌套令牌块
看起来你已经搞定了词法分析的部分,现在卡在了语法块的嵌套表示上——想用结构体但原来的设计没法容纳嵌套的Block(比如表达式里包含标识符块),对吧?我之前做小型解析器的时候也遇到过类似的需求,给你几个不用any的方案,都是基于结构体的,而且比any更靠谱、更符合解析器的类型安全需求:
最推荐:用C++17 std::variant 实现类型安全的嵌套
std::variant 相当于类型安全的标签联合体,它明确限定了能存储的类型集合(比如你的场景里就是Token或Block),比any更可控,不会出现莫名其妙的类型错误,完全适配你用结构体的需求。
具体实现代码
// 假设你已经实现的Token结构体(按你的描述简化) struct Token { std::string value; int type; // 比如你定义的TTYPE_IDTYPE、TTYPE_ID等 }; // 前向声明Block,因为BlockContent要用到它 struct Block; // 定义Block的内容:要么是单个Token,要么是另一个Block using BlockContent = std::variant<Token, Block>; // 用枚举代替裸int表示块类型,可读性更强 enum BlockType { BLOCK_LINE, BLOCK_ID, BLOCK_EXPR }; // 最终的Block结构体,完美支持嵌套 struct Block { std::vector<BlockContent> contents; // 容纳Token或子Block BlockType type; // 标记当前块是行、标识符还是表达式 };
怎么用这个结构构建你示例中的嵌套块
比如你给出的示例行[F]set [V]v2, ([V]v1 = 12),可以这样构建:
// 构建标识符块:[F]set Block idSetBlock; idSetBlock.type = BLOCK_ID; idSetBlock.contents.emplace_back(Token{"[F]", TTYPE_IDTYPE}); idSetBlock.contents.emplace_back(Token{"set", TTYPE_ID}); // 构建标识符块:[V]v2 Block idV2Block; idV2Block.type = BLOCK_ID; idV2Block.contents.emplace_back(Token{"[V]", TTYPE_IDTYPE}); idV2Block.contents.emplace_back(Token{"v2", TTYPE_ID}); // 构建表达式里的标识符块:[V]v1 Block idV1Block; idV1Block.type = BLOCK_ID; idV1Block.contents.emplace_back(Token{"[V]", TTYPE_IDTYPE}); idV1Block.contents.emplace_back(Token{"v1", TTYPE_ID}); // 构建表达式块:[V]v1 = 12 Block exprBlock; exprBlock.type = BLOCK_EXPR; exprBlock.contents.emplace_back(idV1Block); // 嵌套标识符块 exprBlock.contents.emplace_back(Token{"=", TTYPE_OPERATOR}); exprBlock.contents.emplace_back(Token{"12", TTYPE_INTEGER}); // 构建整个行块 Block lineBlock; lineBlock.type = BLOCK_LINE; lineBlock.contents.emplace_back(idSetBlock); lineBlock.contents.emplace_back(idV2Block); lineBlock.contents.emplace_back(exprBlock);
怎么访问嵌套内容
用std::visit可以安全地遍历Block的内容,区分是Token还是子Block,比如写个递归打印函数调试:
#include <iostream> #include <variant> #include <vector> #include <string> // 递归打印Block结构 void printBlock(const Block& block, int indent = 0) { std::string indentStr(indent, ' '); const char* blockName = nullptr; switch(block.type) { case BLOCK_LINE: blockName = "line"; break; case BLOCK_ID: blockName = "id"; break; case BLOCK_EXPR: blockName = "expr"; break; } std::cout << indentStr << blockName << " {\n"; for(const auto& content : block.contents) { std::visit([&](const auto& val) { using T = std::decay_t<decltype(val)>; if constexpr (std::is_same_v<T, Token>) { // 打印Token std::cout << indentStr << " '" << val.value << "' : TTYPE_XXX\n"; } else if constexpr (std::is_same_v<T, Block>) { // 递归打印子Block printBlock(val, indent + 2); } }, content); } std::cout << indentStr << "}\n"; } // 调用示例:printBlock(lineBlock);
兼容C++17之前的方案:手动实现标签联合
如果你的项目不能用C++17,可以自己实现标签联合(Tagged Union),本质是用枚举标记当前联合里存的是什么类型:
struct Token { std::string value; int type; }; struct Block; // 手动实现的Block内容标签联合 struct BlockContent { enum Tag { TOKEN, BLOCK } tag; union { Token token; Block block; }; // 因为union里有非POD类型(Block里的vector),需要手动管理构造/析构 BlockContent(const Token& t) : tag(TOKEN), token(t) {} BlockContent(const Block& b) : tag(BLOCK), block(b) {} BlockContent(const BlockContent& other) { if (other.tag == TOKEN) { tag = TOKEN; new (&token) Token(other.token); } else { tag = BLOCK; new (&block) Block(other.block); } } ~BlockContent() { if (tag == TOKEN) token.~Token(); else block.~Block(); } }; enum BlockType { BLOCK_LINE, BLOCK_ID, BLOCK_EXPR }; struct Block { std::vector<BlockContent> contents; BlockType type; };
不过这个方案需要手动管理析构和拷贝,容易出错,所以优先推荐std::variant。
为什么不推荐用any?
any虽然能存任意类型,但它的类型信息是动态的:
- 你永远不知道里面实际存的是什么,访问时需要强制类型转换,很容易写出运行时错误;
- 调试的时候很难直接看到
any里的内容,排查问题麻烦; - 性能比
variant差,因为any通常需要堆分配,而variant是栈分配(除非内容本身是堆分配的,比如vector)。
总结
用std::variant的方案完全符合你的需求:
- 全程用结构体(
Token、Block都是结构体),没有复杂的类继承; - 完美支持嵌套块(Block里可以放Token或另一个Block);
- 类型安全,编译时就能检查类型错误,调试和维护都方便。
内容来源于stack exchange
相关产品推荐
相关产品推荐

