I have a memory management question. For one of my projects I am building an interpreter for a small programming language. One of the first steps is to model and build an Abstract Syntax Tree.
As of now, I'm using smart pointers to manage the lifetime of nodes, and I figured that every parent node is the owner of it's children, but it also must be shared with the environment (for example, to know which part of the tree a method's body belongs to) and with the garbage collector, which must keep a list of all references to enact the naïve mark and sweep algorithm. Therefore, I am using std::shared_ptr
to keep track of the references. For instance, here's an example of a Block
node, which basically represents a lambda expression:
#ifndef NAYLANG_BLOCK_H
#define NAYLANG_BLOCK_H
#include <model/ast/expressions/Expression.h>
#include <model/ast/declarations/Declaration.h>
#include <memory>
#include <vector>
namespace naylang {
#define BlockPtr std::shared_ptr<Block>
class Block : public Expression {
std::vector<std::shared_ptr<Statement>> _body;
std::vector<std::shared_ptr<Declaration>> _params;
public:
Block() = default;
void accept(Evaluator &evaluator) override;
const std::vector<std::shared_ptr<Statement>> &body() const;
const std::vector<std::shared_ptr<Declaration>> ¶ms() const;
void addStatement(std::shared_ptr<Statement> statement);
void addParameter(std::shared_ptr<Declaration> param);
};
} // end namespace naylang
#endif //NAYLANG_BLOCK_H
As you can see, this node is the owner of all it's parameters and body expressions, and has accessors so that the Evaluator
can traverse the tree.
Now, the problem comes when trying to have nodes that are bound at evaluation time to other nodes, for example:
#ifndef NAYLANG_REQUEST_H
#define NAYLANG_REQUEST_H
#include <model/ast/expressions/Expression.h>
#include <string>
#include <memory>
#include <vector>
#include <model/ast/declarations/MethodDeclaration.h>
namespace naylang {
class Request : public Expression {
std::string _name;
std::vector<ExpressionPtr> _params;
// We use naked pointers because we don't want to worry
// about memory management, and there is no ownership
// with the declaration.
const MethodDeclaration *_binding;
public:
Request(const std::string &methodName);
Request(const std::string &methodName, const std::vector<ExpressionPtr> params);
void accept(Evaluator &evaluator) override;
void bindTo(const MethodDeclaration *_binding);
const std::string &method() const;
const std::vector<ExpressionPtr> ¶ms() const;
const MethodDeclaration &declaration() const;
};
} // end namespace naylang
#endif //NAYLANG_REQUEST_H
As you can see, bindTo()
is called when a BindingEvaluator
(subclass of Evaluator
) evaluates a Request object, long after it's constructed. However, I am really not sure about what the _binding
parameter should look like. Here's a part of the Evaluator
interface:
#ifndef NAYLANG_EVALUATOR_H
#define NAYLANG_EVALUATOR_H
#include <model/ast/Statement.h>
namespace naylang {
class Request;
class Block;
class Evaluator {
public:
Evaluator() = default;
virtual ~Evaluator() = default;
// Methods left blank to be overridden by the subclasses.
// For example, a Binding Evaluator might be only interested in
// evaluating VariableReference and Request Statements
virtual void evaluate(Request &expression) {}
virtual void evaluate(Block &expression) {}
};
}
#endif //NAYLANG_EVALUATOR_H
Here's my rationale:
- The reference should be polymorphic, and therefore should be some kind of pointer.
- The reference does not denote ownership, and therefore it should not be a
std::shared_ptr
. - In addition, we need the Visitor pattern, so every node has the function
void accept(Evaluator &evaluator);
. As a node cannot return ashared_ptr
of itself, we cannot change the interface to something likevirtual void evaluate(std::shared_ptr<Request> &expression) {}
.
Thus, naked pointers. I really want to get this right before moving on, because it's a ton of code to change every time I rethink it (ASTs are verbose...)
Thank you in advance.
Aucun commentaire:
Enregistrer un commentaire