2010-09-23 16 views
5

Pour ma propre cadre peu d'analyse syntaxique, je suis en train de définir (quelque chose comme) la fonction suivante:"copie carbone" un istream C++?

template <class T> 
// with operator>>(std::istream&, T&) 
void tryParse(std::istream& is, T& tgt) 
{ 
    is >> tgt /* , *BUT* store every character that is consumed by this operation 
    in some string. If afterwards, is.fail() (which should indicate a parsing 
    error for now), put all the characters read back into the 'is' stream so that 
    we can try a different parser. */ 
} 

alors je pourrais écrire quelque chose comme ceci: (peut-être pas le meilleur exemple)

/* grammar: MyData  = <IntTriple> | <DoublePair> 
      DoublePair = <double> <double> 
      IntTriple = <int> <int> <int> */ 
class MyData 
{ public: 
    union { DoublePair dp; IntTriple it; } data; 
    bool isDoublePair; 
}; 

istream& operator>>(istream& is, MyData& md) 
{ 
    /* If I used just "is >> md.data.it" here instead, the 
     operator>>(..., IntTriple) might consume two ints, then hit an 
     unexpected character, and fail, making it impossible to read these two 
     numbers as doubles in the "else" branch below. */ 
    tryParse(is, md.data.it); 
    if (!is.fail()) 
     md.isDoublePair = false; 
    else 
    { 
     md.isDoublePair = true; 
     is.clear(); 
     is >> md.data.dp; 
    } 
    return is; 
} 

Toute aide est grandement appréciée.

+0

Les flux ne sont pas l'outil idéal pour cela, en raison de leur manque de recul approprié. Lors de la conception de simples analyseurs en ligne de ce type (sinon, essayez d'utiliser boost :: spirit), les fonctions d'analyse devraient utiliser une paire d'itérateurs. Il devient facile de revenir en arrière (il suffit de sauvegarder la valeur de l'itérateur avant un analyseur de retour arrière). –

Répondre

3

Malheureusement, les cours d'eau n'ont qu'un support de recul très minimal et rudimentaire. La dernière fois que j'en ai eu besoin, j'ai écrit mes propres classes de lecteurs qui enveloppaient un flux, mais qui avaient un tampon pour remettre les choses dans le flux et les lire seulement quand ce tampon est vide. Ceux-ci avaient des moyens d'obtenir un état, et vous pourriez commettre un état ou un retour à un état antérieur. L'action par défaut dans le destructeur de la classe d'état consistait à annuler, afin que vous puissiez analyser sans réfléchir à la gestion des erreurs, car une exception annulait simplement l'état de l'analyseur jusqu'à un point où une règle de grammaire différente était essayée. (Je pense que ce qu'on appelle retours en arrière.) Voici un croquis:

class parse_buffer { 
    friend class parse_state; 
public: 
    typedef std::string::size_type index_type; 

    parse_buffer(std::istream& str); 

    index_type get_current_index() const; 
    void set_current_index(index_type) const; 

    std::string get_next_string(bool skip_ws = true) const; 
    char get_next_char(bool skip_ws = true); 
    char peek_next_char(bool skip_ws = true); 

    std::string get_error_string() const; // returns string starting at error idx 
    index_type get_error_index() const; 
    void set_error_index(index_type); 

    bool eof() const; 

    // ... 
}; 

class parse_state { 
public: 
    parse_state(parse_buffer&); 
    ~parse_state(); 

    void commit(); 
    void rollback(); 

    // ... 
}; 

Cela devrait vous donner une idée. Il n'a pas de mise en œuvre, mais c'était simple et devrait être facile à refaire. En outre, le code réel avait de nombreuses fonctions pratiques comme lire des fonctions qui lisaient une chaîne délimitée, consommait une chaîne s'il s'agissait de plusieurs mots clés donnés, lisait une chaîne et la convertissait en un type donné par paramètre de modèle, et ainsi de suite. L'idée était qu'une fonction placerait l'index d'erreur à sa position de départ, enregistrerait l'état d'analyse, et essaierait d'analyser jusqu'à ce qu'elle ait réussi ou ait couru dans une impasse. Dans ce dernier cas, il ne ferait que lancer une exception. Cela détruirait les objets parse_state sur la pile, ramenant l'état à une fonction qui pourrait intercepter l'exception et essayer quelque chose d'autre, ou générer une erreur (qui correspond à get_error_string().)

Si vous voulez Parser vraiment rapide, cette stratégie peut-être mal, mais les flux sont souvent ralentir, aussi. OTOH, la dernière fois que j'ai utilisé quelque chose comme ça, j'ai fait un analyseur XPath qui fonctionne sur un DOM propriétaire, qui est utilisé pour représenter des scènes dans un moteur de rendu 3D.Et c'était pas le l'analyseur XPath qui a obtenu toute la chaleur des gars essayant d'obtenir des cadences plus élevées. :)

+0

Cela semble très intéressant. Avez-vous encore le code et est-il open source/puis-je le regarder? – rainmaker

+0

Oh, comme c'est gentil! Donc, parse_state :: rollback() appelle simplement set_current_index (index_on_my_creation) sur son parse_buffer? et commit() ne - uhm - rien, laissant l'index de parse_buffer où il est? Ah, mais commit() pourrait dire à parse_buffer que nous n'allons pas rollback() avant la position actuelle et donc il est sûr (au moins pour cette parse_state) d'oublier tout ce qui précède cette position ... Oui, je pense que je commence comprendre. Je vais essayer ça, merci beaucoup! – rainmaker

+0

@rainmaker: Oui, vous avez eu l'idée. Une autre chose consiste à définir l'index d'erreur à la position actuelle, de sorte que les erreurs d'analyse ultérieures donnent l'index actuel comme où le texte défectueux a commencé. Je me souviens vaguement que cela avait ses coins et recoins (est-ce que 'parse_state' ne devrait pas non plus stocker l'ancien index d'erreur?), Mais il y a plusieurs années que j'ai utilisé ce schéma, donc, je devrais refais cela, je n'aurais pas plus que le dessus pour commencer. ':)' – sbi

3

Ce n'est pas ce que les flux sont destinés. Vous devriez lire les données que vous voulez analyser dans un tampon, puis remettre ce tampon (de préférence en tant que plage d'itération) aux fonctions qui l'analysent. Cela pourrait ressembler à ceci:

template <class T, class U> 
bool tryParse(U & begin, U & end, T & target) { 
    // return true if parse was successful, false otherwise 
} 

Pour lire un istream dans un tampon, vous pouvez utiliser un istream_iterator:

std::vector<char> buffer(std::istream_iterator<char>(is), std::istream_iterator<char>()); 

Ce lit tout le flux dans le vecteur lors de sa création.

2

Remettre les caractères est difficile. Certains flux prennent en charge unget() et putback(somechar), mais il n'existe aucune garantie sur le nombre de caractères que vous pouvez désengager (le cas échéant).

Une méthode plus fiable consiste à lire les caractères dans un tampon et à les analyser ou à stocker les caractères lus lors de la première tentative d'analyse et à utiliser ce tampon lors de l'analyse une seconde fois.

+0

+1 pour l'analyse d'un tampon séparé. –

1

Vous pouvez faire des choses intéressantes avec les membres du flux streambuf. En particulier, vous avez un accès direct aux pointeurs des tampons.

Cependant, vous n'avez aucune garantie sur la taille des tampons.