是否从函数未定义的行为返回取消引用的指针作为引用?

时间:2019-06-17 22:25:46

标签: c++ language-lawyer

我是第一次写解析器。我正在关注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;
}

信不信由你,我认为这是很小的。记住,这是一个解析器。请记住,作为一个最小的示例,我计划对其进行扩展,但是希望您能理解。

2 个答案:

答案 0 :(得分:4)

您希望使用多态-有两种方法。使用引用或指针。带有引用的东西是当您返回它们时很危险。在大多数时候,UB返回对本地对象的引用。那意味着我们剩下指针了。

但是不要使用newdelete。它们是不安全的,难以处理,尤其是在多范围环境中。使用智能指针。使用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页规则,从自动存储持续时间的工作方式,解引用的工作方式,左值的工作方式到如何复制的工作原理,继承的工作原理,虚拟成员函数的工作原理等,等等。