我是第一次写解析器。我正在关注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);
这是我的问题:如果这样做,我将丢失vtable和新Expression
中的所有额外数据。这是一个问题,因为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和其他数据,并且不会立即销毁它。这样我就可以吃蛋糕了。
但是,我的问题是我可以实际执行此操作吗?虽然我有充分的理由这样做,但在我看来这很奇怪。我正在函数内分配新数据,并希望像任何普通变量一样将其自动释放到函数外。即使该 did 工作正常,它的行为是否也会完全超出此功能的范围?我担心这可能会调用未定义的行为或类似的东西。标准对此有何看法?
编辑:所以这是要求的最小样本:
表达:
// 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;
}
信不信由你,我认为这是很小的。记住,这是一个解析器。请记住,作为一个最小的示例,我计划对其进行扩展,但是希望您能理解。
答案 0 :(得分:4)
您希望使用多态-有两种方法。使用引用或指针。带有引用的东西是当您返回它们时很危险。在大多数时候,UB返回对本地对象的引用。那意味着我们剩下指针了。
但是不要使用new
和delete
。它们是不安全的,难以处理,尤其是在多范围环境中。使用智能指针。使用unique_ptr
:
#include <memory>
struct expression {
virtual void foo() = 0;
virtual ~expression() = default;
};
struct prefix_expression : expression {
virtual void foo() { /* default impl */ }
// dummy c-tor
prefix_expression(int) {}
};
// note that parse() returns a pointer to any *expression*!
std::unique_ptr<expression> parse() {
// pass to make_unique whatever arguments the constructor of prefix_expression needs
return std::make_unique<prefix_expression>(42);
}
int main() {
{
auto expr = parse();
// here, *expr* goes out of score and properly deletes whatever it has new-ed
}
}
编辑:
还要回答标题中的问题-否。
答案 1 :(得分:4)
您是对的-您需要一个指针,并且要进行范围界定,您需要动态分配。
Java已经在幕后为您做到了。
请不要使用new
,而要使用智能指针,这样它不会变得混乱。
我们不能为此提供“标准引号”,因为我们必须引述20或30页规则,从自动存储持续时间的工作方式,解引用的工作方式,左值的工作方式到如何复制的工作原理,继承的工作原理,虚拟成员函数的工作原理等,等等。