2012-11-29 4 views
4

J'écris un interpréteur pour un langage de programmation simple, de type Lisp. Il va traiter le code en nœuds, tous ont des types et certains d'entre eux peuvent avoir des nœuds enfants dans un ordre indexé. En raison de la différence dans la nature de l'information, je ne peux pas utiliser la même longueur de type pour toutes les valeurs de nœud. Le nom de leur type est un type enum, mais la seule idée que j'ai pour le type de valeurs est void *. Mais quand je l'utilise, je dois être très prudent, je pense. Je veux dire, je ne peux pas utiliser de destructeurs par défaut, je dois écrire un destructeur qui se soucie du type de nœud. Aussi, je devrais utiliser beaucoup de moulages, même pour accéder aux valeurs.Quelle est la meilleure façon de gérer les types inconnus dans une structure C++?

C'est ce dont je parle:

enum NodeType {/* Some node types */} 

class Node 
{ 
public: 
    Node(string input_code); 
private: 
    NodeType type; // Having this I can know the type of value 
    void* value; 
}; 

Y at-il une manière qui est plus sûr, fait un meilleur code, mais est toujours aussi efficace que l'utilisation de pointeurs vides?

+4

Vous pouvez utiliser une hiérarchie de classes/un héritage. – melpomene

Répondre

5

Il y a deux options auxquelles je peux penser. La première consiste à utiliser le polymorphisme, dans lequel vous avez une classe de base abstraite, Node et un certain nombre de sous-classes spécifiques au type. Peut-être quelque chose comme:

class Node 
{ 
public: 
    virtual ~Node() = 0; 
}; 

class String : public Node 
{ 
public: 
    ~String() {} 
}; 

class Float : public Node 
{ 
public: 
    ~Float() {} 
}; 

Lorsque le stockage de ces nœuds, vous stockerait Node* plutôt que void*. La présence de la (abstraite) destructor virtuelle dans la classe de base permet de bien détruire des objets concrets via le pointeur de la classe de base, comme:

Node* obj = new String; 
delete obj; 

Vous pouvez également appeler des méthodes déclarées dans la classe de base et de les exécute du code dans la classe dérivée correcte, si ces méthodes sont virtuelles dans la classe de base. ceux-ci seront souvent aussi pures virtuals, comme avec:

class Node 
{ 
public: 
    std::string Speak() const = 0; // pure virt 
}; 

class String : public Node 
{ 
public: 
    std::string Speak() const { return "Hello"; } 
}; 

Une autre option est d'utiliser une sorte de classe variante. C++ lui-même n'a pas de classe de variante intégrée au langage, mais certaines bibliothèques ont été écrites comme Boost, qui fournissent une telle classe.

+0

Merci! C'est inspirant. Mais je ne comprends pas, comment ce destructeur pourrait-il connaître la longueur de la mémoire allouée? Comme il ne semble même pas connaître le type lui-même. Je ne suis pas un programmeur C++ expérimenté. Est-ce que C++ stocke la longueur des allocations de mémoire dans une table? Est-il possible de libérer une section allouée de la mémoire dynamique en ne connaissant que le pointeur du premier octet? – kdani

+0

Tout cela fait partie de la magie du polymorphisme. Le standard C++ dicte que si vous supprimez un objet de classe dérivée via un pointeur de classe de base, et que la classe de base a un destructeur 'virtual', alors le destructeur de la classe dérivée est appelé. Bien sûr, ce n'est pas vraiment magique comment cela fonctionne, mais le Standard ne dicte pas * comment * cela fonctionne, mais seulement qu'il le devrait. En réalité, la plupart des compilateurs modernes implémentent cela en termes de tables v stockées à côté de l'objet lui-même. –

0

Vous devez utiliser une sorte de type de variante. Boost en a un et les détails peuvent être trouvés here.

2

utiliser l'héritage/Interfaces:

struct BaseNode { 
    std::vector<BaseNode*> _children; 
    /*Some functions here*/ 
} 

Pour chaque type dans le enum NodeType font une nouvelle classe qui hérite BaseNode.

+0

C'est une bonne idée de cette façon que je peux même jeter cette énumération. Mais comment puis-je gérer ces valeurs qui ne sont même pas des vecteurs? Juste des types entiers, chaîne ou autre chose?En raison de la structure de ma langue, je pense que le plus simple serait de les traiter comme des listes. (Certains types de liste seraient également évalués à des types de longueur fixe, mais pas tous) – kdani

+0

@kdani - Je pense que John Dibling donne une bonne solution à ce problème. – andre

0

Voici un croquis rapide d'un système de nœud basé boost::variant:

class Node; 
struct Empty {}; 

// Keep enum and variant in sync: 
enum NodeType { 
    eEmptyNode, 
    eStringNode, 
    eIntNode, 
    eListNode, 
}; 
typedef std::vector< const Node > NodeList; 
typedef boost::variant< std::string, int, NodeList > NodeData; 

// Keep this in sync with types in Node and enum: 
NodeType GetNodeType(Empty const&) { return eEmptyNode; } 
NodeType GetNodeType(std::string const&) { return eStringNode; } 
NodeType GetNodeType(int const&) { return eIntNode; } 
NodeType GetNodeType(NodeList const&) { return eListNode; } 


// Some helper code: 
struct GetNodeType_visitor 
{ 
    typedef NodeType return_type; 
    template<typename T> 
    NodeType operator()(T const& t) const { return GetNodeType(t); } 
}; 

template<typename T, typename Function> 
struct OneType_visitor 
{ 
    typedef bool return_type; 
    Function func; 
    OneType_visitor(Function const& f):func(f) {} 
    template<typename U> 
    bool operator()(U const& u) const { return false; } 
    bool operator()(T const& t) const { func(t); return true; } 
}; 

struct Node 
{ 
    NodeData data; 
    NodeType GetType() { return boost::apply_visitor(GetNodeType_visitor, data); } 
    template<typename T, typename Function> 
    bool Apply(Function const& func) const 
    { 
    return boost::apply_visitor(OneType_visitor<T>(func), data); 
    } 
    template<typename T> 
    Node(T const& t):data(t) {} 
    Node():data(Empty()) {} 
}; 

// example usage: 
int main() 
{ 
    NodeList nodes; 
    nodes.push_back(Node<int>(7)); 
    nodes.push_back(Node<std::string>("hello")); 
    Node root(nodes); 

    Assert(root.GetType() == eListNode); 
    std::function<void(Node const&)> PrintNode; 
    auto PrintInt = [](int const& i) { std::cout << "#" << i; }; 
    auto PrintString = [](std::string const& s) { std::cout << "\"" << s << "\""; }; 
    auto PrintList = [&](NodeList const& list) { 
    std::cout << "["; 
    for (auto it = list.begin(); it !=list.end(); ++it) 
    { 
     if (it != list.begin()) std::cout << ","; 
     PrintNode(*it); 
    } 
    std::cout << "]"; 
    } 
    auto PrintEmpty = [](Empty const&) { std::cout << "{}"; } 
    PrintNode = [&](Node const& n) 
    { 
    bool bPrinted = false; 
    bPrinted = n.Apply<int>(PrintInt) || bPrinted; 
    bPrinted = n.Apply<std::string>(PrintString) || bPrinted; 
    bPrinted = n.Apply<NodeList>(PrintList) || bPrinted; 
    bPrinted = n.Apply<Empty>(PrintEmpty) || bPrinted; 
    Assert(bPrinted); 
    } 
    PrintNode(root); 
} 
Code

n'est pas testé, mais devrait tenir l'idée de base.

Notez que j'utilise des nœuds immuables, comme c'est le cas pour un langage de type lisp. Vraiment, je devrais utiliser std::shared_ptr<const Node> ou quelque chose comme ça, donc deux arbres peuvent partager des données.

boost::variant gère le problème de typage dynamique.

Questions connexes