【发布时间】:2019-06-17 22:25:46
【问题描述】:
我是第一次编写解析器。我正在关注this tutorial on Pratt parers。我已经让它工作了,但我想出了一个问题。
原始教程是用 Java 编写的。我更喜欢 C++,所以这就是我写的。我基本上能够将大部分代码移植到 C++(尽管我确实将其设为“我的”,因为存在一些与语言无关的差异)。我唯一真正的问题是这行代码:
public Expression parse(Parser parser, Token token) { Expression operand = parser.parseExpression(); ? return new PrefixExpression(token.getType(), operand);
这在 Java 中运行良好(我假设。我以前从未真正使用过 Java,但我认为这家伙知道他在做什么),但在 C++ 中却没有这么多。我能够通过使用这样的指针来完成同样的事情:
Expression* parse(Parser& parser, Token token) {
Expression* operand = parser.parseExpression();
return new PrefixExpression(token.getType(), operand);
这(虽然我不熟悉 Java 的语义)似乎在 C++ 中做同样的事情,只是使用指针而不是普通对象。
但是,使用这样的指针的问题在于它会很快变得混乱。现在使用指针变得更容易,这意味着我不得不担心释放,如果我做得不对,可能会出现内存泄漏。它只是变得一团糟。
现在,解决方案似乎很简单。我可以像这样返回PrefixExpression:
Expression parse(Parser& parser, Token token) {
Expression operand = parser.parseExpression();
return PrefixExpression(token.getType(), operand);
这是我的问题:如果我这样做,我会丢失这个新Expression 中的 vtable 和任何额外数据。这是一个问题,因为Expression 实际上只是许多类型表达式的基类。 Parse 可以解析任何它想要解析的东西,而不仅仅是 PrefixExpression。原来的设计就是这样。一般来说,我喜欢这种设计,但是,正如你所看到的,它会引起问题。在这里简单地返回一个新的Expression 会丢失我以后从该对象中需要的东西。
现在,我可以尝试通过返回参考来解决这个问题:
Expression& parse(Parser& parser, Token token) {
// ...
return PrefixExpression(token.getType(), operand);
这解决了 vtable 和额外数据问题,但现在创建了一个新问题。我正在返回对将立即销毁的变量的引用,这无济于事。
所有这些,这就是我最初最终选择指针的原因。指针让我可以保留以后需要的数据,但它们真的很难使用。我可以挤过去,但我个人想要更好的东西。
我想我可以使用std::move,但我对此不够熟悉,无法确定我会正确使用它。如果我必须这样做,我会这样做,但是正确地实施它需要一些我没有的技能和知识。此外,要返工到目前为止我必须以这种方式工作的所有内容,需要做大量工作。
所有这些都引出了我的问题的要点:我可以简单地安全地返回对新对象的引用吗?让我举个例子:
Expression& parse(Parser& parser, Token token) {
//...
return *(new PrefixExpression(token.getType(), operand));
这会很好并解决我的大部分问题,因为如果它按照我的想法执行,我将获得对新对象的引用,保留 vtable 和额外数据,并且它不会立即被销毁。这样我就可以吃蛋糕了。
但是,我的问题是我真的可以这样做吗?虽然我觉得我有充分的理由这样做,但这对我来说似乎很奇怪。我在函数内部分配新数据,并期望它像任何普通变量一样在函数外部自动释放。即使 确实 工作,它会像我期望的那样完全在这个函数之外吗?我担心这可能会调用未定义的行为或类似的东西。标准对此有何看法?
编辑:所以这是一个请求的最小示例:
表达式:
// A (not really pure) purely virtual base class that holds all types of expressions
class Expression {
protected:
const std::string type;
public:
Expression() : type("default") {}
virtual ~Expression() {} //Because I'm dealing with pointers, I *think* I need a virtual destructor here. Otherwise, I don't really need
virtual operator std::string() {
// Since I am working with a parser, I want some way to debug and make sure I'm parsing correctly. This was the easiest.
throw ("ERROR: No conversion to std::string implemented for this expression!");
}
// Keep in mind, I may do several other things here, depending on how I want to use Expression
};
一个孩子Expression,用于括号:
class Paren : public Expression {
private:
// Again, Pointer is not my preferred way, but this was just easier, since Parse() was returning a pointer anyway.
Expression* value;
public:
Paren(Expression *e) {
// I know this is also sketchy. I should be trying to perform a copy here.
// However, I'm not sure how to do this, since Expression could be anything.
// I just decided to write my code so the new object takes ownership of the pointer. I could and should do better
value = e;
}
virtual operator std::string() {
return "(" + std::string(*value) + ")";
}
// Because again, I'm working with pointers
~Paren() {delete value;}
};
还有一个解析器:
class Parser {
private:
Grammar::Grammar grammar;
public:
// this is just a function that creates a unique identifier for each token.
// Tokens normally have types identifier, number, or symbol.
// This would work, except I'd like to make grammar rules based off
// the type of symbol, not all symbols in general
std::string GetMapKey(Tokenizer::Token token) {
if(token.type == "symbol") return token.value;
return token.type;
}
// the parsing function
Expression * parseExpression(double precedence = 0) {
// the current token
Token token = consume();
// detect and throw an error here if we have no such prefix
if(!grammar.HasPrefix(GetMapKey(token))) {
throw("Error! Invalid grammar! No such prefix operator.");
}
// get a prefix parselet
Grammar::PrefixCallback preParse = grammar.GetPrefixCallback(GetMapKey(token));
// get the left side
Expression * left = preParse(token,*this);
token = peek();
double debug = peekPrecedence();
while(precedence < peekPrecedence() && grammar.HasInfix(GetMapKey(token))) {
// we peeked the token, now we should consume it, now that we know there are no errors
token = consume();
// get the infix parser
Grammar::InfixCallback inParse = grammar.GetInfixCallback(GetMapKey(token));
// and get the in-parsed token
left = inParse(token,left,*this);
}
return left;
}
在我发布了解析器代码之后,我意识到我应该提到我将所有与语法相关的东西都放到了它自己的类中。它只是有一些与语法相关的不错的实用程序,并且允许我们编写一个独立于语法的解析器并在以后担心语法:
class Grammar {
public:
// I'm in visual studio 2010, which doesn't seem to like the using type = value; syntax, so this instead
typedef std::function<Expression*(Tokenizer::Token,Parser&)> PrefixCallback;
typedef std::function<Expression*(Tokenizer::Token, Expression*, Parser&)> InfixCallback;
private:
std::map<std::string, PrefixCallback> prefix;
std::map<std::string, InfixCallback> infix;
std::map<std::string, double> infixPrecedence; // we'll use double precedence for more flexabillaty
public:
Grammar() {
prefixBindingPower = std::numeric_limits<double>::max();
}
void RegisterPrefix(std::string key, PrefixCallback c) {
prefix[key] = c;
}
PrefixCallback GetPrefixCallback(std::string key) {
return prefix[key];
}
bool HasPrefix(std::string key) {
return prefix.find(key) != prefix.end();
}
void RegisterInfix(std::string key, InfixCallback c, double p) {
infix[key] = c;
infixPrecedence[key] = p;
}
InfixCallback GetInfixCallback(std::string key) {
return infix[key];
}
double GetInfixPrecedence(std::string key) {
return infixPrecedence[key];
}
bool HasInfix(std::string key) {
return infix.find(key) != infix.end();
}
};
最后,我可能需要显示一个解析回调来完成设置:
Expression* ParenPrefixParselet(Tokenizer::Token token, Parser& parser) {
Expression* value = parser.parseExpression(0);
Expression* parenthesis = new Paren(value); // control of value gets given to our new expression. No need to delete
parser.consume(")");
return parenthesis;
}
这使我可以编写一个语法,允许括号中的内容如下:
Grammar g;
g.RegisterPrefix("(", &ParenPrefixParselet);
最后是一个 main():
int main() {
Grammar g;
g.RegisterPrefix("(", &ParenPrefixParselet);
Parser parser(g);
Expression* e = parser.parseExpression(0);
std::cout << static_cast<std::string>(*e);
return 0;
}
信不信由你,我认为这非常小。请记住,这是一个解析器。请记住,作为一个最小示例,我计划对其进行扩展,但希望您能理解。
【问题讨论】:
-
不可能删除该对象,因为您使用 new 然后丢弃指针。但我的第一印象不是UB,只是内存泄漏。我的第一直觉是只返回一个对象并让 c++ 处理它。 "return PrefixExpression(token.getType(), 操作数);"
-
"这是我的问题:如果我这样做,我会丢失 vtable 和这个新
Expression中的任何额外数据" - 为什么?PrefixExpression构造函数消耗的相关数据不是吗?您的对象是否遵循Rule of three/five/zero? -
@Fureeish 因为
Expression是一个抽象类。它没有真正的实现。Expression本身有一个虚拟析构函数,但除此之外它遵循 0 规则,我认为——除非单独拥有一个虚拟析构函数就违反了 0/3/5 规则 -
可能需要一个工厂构造函数。你能做一个最小的代码示例,我们可以处理任何问题。如果您要保留指向抽象基类的指针集合,则需要构造实际的具体类。
-
@Fureeish 请记住,我返回的是
Expression,而不是PrefixExpression。上杠杆解析代码只看到一个表达式。如果我需要来自PrefixExpression的东西,理论上我需要投射它。
标签: c++ language-lawyer