【问题标题】:Is returning a dereferenced pointer as a reference from a function undefined behavior?是否将取消引用的指针作为函数未定义行为的引用返回?
【发布时间】: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


【解决方案1】:

您希望使用多态性 - 有两种方法。使用引用或指针。引用的事情是,当您返回它们时很危险。当您返回对本地对象的引用时,大多数情况下都是 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
    }
}

编辑:

还要回答标题中的问题 -

【讨论】:

  • 感谢您竭尽全力帮助我,但我仍然对标准的意见感兴趣。我正在寻找标准的引用。
  • "我正在寻找标准的报价" - 报价到底是什么?
  • 在函数范围内返回对在堆中分配的数据的引用。我知道返回对本地对象的引用是不好的,因为引用很快就会被破坏。我想知道的是,使用new 会改变它并让它几乎像外界的正常变量一样工作吗?标准是怎么说的?
  • @Chipster 我相信如果你这样做int&amp; foo() { return *new int{}; }; ... auto&amp; x = foo(); delete &amp;x;,代码应该按预期运行。但我会强烈反对这种想法。首先,您需要记住通过引用捕获返回类型。然后你需要记住delete &amp;x,看起来,嗯,。最后,你没有范围保护——如果你没有delete,你最终会出现内存泄漏。智能指针解决了所有这些问题。
  • 太棒了。谢谢。我想,这正是我所需要的。
【解决方案2】:

你是对的 - 你需要一个指针,并且要绕过范围,你需要动态分配。

Java 已经在幕后为您做到了。

不要使用new,但是,使用智能指针,这样它不会变得混乱。

我们不能就此提供“标准的引用”,因为我们必须引用 20 或 30 页的规则,从自动存储持续时间的工作原理到取消引用的工作原理、左值的工作原理以及复制工作,继承如何工作,虚拟成员函数如何工作等等。

【讨论】:

  • 好的。那么也许是关于在该函数范围内返回对堆中分配的内存的引用的引用?我几乎不认为关于“虚拟成员函数如何工作”或“继承如何工作”,甚至“复制如何工作”的引用在这里完全相关(除了可能是因为我的示例使用了所有这些东西,但我因为它使用所有这些事情,那么我必须知道它是如何工作的......)。我的问题只是关于使用new 来返回对对象的引用并避免使用指针语义。
  • @Chipster 是什么让您认为标准中有关于“在该函数范围内返回对在堆中分配的内存的引用”的特定引用?有一章是关于函数的,一章是关于动态分配的,一章是关于取消引用的,还有一章是关于引用的……它没有对每个可以想象的用例进行引用。我并没有真正遵循您要查找的内容以及原因。您对提议的解决方案有疑问/怀疑吗?如果是这样,你应该说明它。
  • 毫无疑问。我认为那是因为我是个白痴 :D 我现在明白了。感谢您的帮助。
  • 哈哈没问题。
猜你喜欢
  • 2017-02-17
  • 2011-10-11
  • 1970-01-01
  • 2023-03-11
  • 2018-07-03
  • 2019-01-06
  • 2018-07-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多