Nous considérons une opération de recherche d’une expression régulière dans un fichier. Pour ce faire, il est nécessaire de pouvoir charger en mémoire une certain nombre de caractères du fichier. Et nous allons chercher dans ce buffer, l’expression régulière concernée. Nous continuons à charger le buffer suivant et nous continuons la recherche jusqu’à ce que l’on arrive à la fin du fichier.
L’expression régulière suivante recherche une définition d’une variable
std::regex match_variables( "^([A-Za-z_][A-Za-z_0-9]*)\\s*=\\s*(.*)$", std::regex_constants::ECMAScript);
qui est définie selon la syntaxe :
... VAR_NAME=VAR_CONTENT NEXT_VAR_NAME=VAR_CONTENT ...
Le code suivant crée un buffer d’une taille adéquate et recherche dans ce buffer l’expression régulière.
std::regex match_variables( "^([A-Za-z_][A-Za-z_0-9]*)\\s*=\\s*(.*)$", std::regex_constants::ECMAScript); std::map<std::string, std::string> find_all_variables(std::string filename) { size_t buffer_size = 1024; using iterator = char*; char* buffer = new char[1024]; std::map<std::string, std::string> variables; try { std::ifstream stream(filename); ptrdiff_t start = 0; while(!stream.eof() && !stream.fail()) { // Load the buffer stream.read(buffer, buffer_size); size_t number_of_available_chars = stream.eof() ? (size_t)stream.gcount() : buffer_size; // Look inside the buffer for all patterns that // matches a variable declaration. iterator current_iterator = buffer; iterator end_iterator = buffer + number_of_available_chars; std::match_results<iterator> match; while(current_iterator != end_iterator && std::regex_search( current_iterator, end_iterator, match, match_variables)) { variables[match[1].str()] = match[2].str(); current_iterator = match[0].second; } } delete[] buffer; } catch(...) { delete[] buffer; throw; } return variables; }
Tester le code précédent ? Expliquer pourquoi le bloc try {} catch(...) {}
a été introduit dans le code ?
Nous souhaitons ne pas utiliser directement des pointeurs. Proposez une écriture permettant d’éviter d’allouer de la mémoire dans le tas, mais dans la pile.
En fait, le code précédent n’est pas correct parce que nous lisons le flux en chargeant des blocs de taille fixe. Nous ne devrions pas lire des blocs de taille fixe, mais des blocs de taille variable correspondant à une ligne (se terminant par un caractère de fin de ligne).
Les flux offrent la possibilité de lire une ligne (s’arrêtant au prochain retour à la ligne ou à la fin d’une ligne).
Nous souhaitons modifier le code précédent pour :
Nous nous proposons d’utiliser les smart pointers et plus précisément les ''%%std::unique_ptr%%''. Ces pointeurs définissent une référence unique sur un objet ou un tableau d’objets alloués dans le tas. Si le pointeur est détruit, puisqu’il s’agit de la seule référence à ce tableau d’objet, la zone mémoire associée à ce pointeur est automatique libérée.
Pour créer un tableau de caractères, il suffit d’écrire :
size_t buffer_size = 80; auto memory = std::make_unique<char[]>(buffer_size);
Si nous souhaitons effectuer un redimensionnement d’un vecteur, nous pouvons imaginer le code suivant :
{ size_t new_buffer_size = buffer_size + 80; auto new_memory = std::make_unique<char[]>(new_buffer_size); std::copy(memory, memory + buffer_size, new_memory); buffer_size = new_buffer_size; std::swap(memory, new_memory); }
Expliquez le fonctionnement du code précédent. Pourquoi empêche-t-il d’avoir une fuite mémoire ?
À partir du code précédent, modifier pour ne pas lire des buffers de taille fixe, mais des buffers pouvant contenir toute la ligne courante. On lira en utilisant la méthode ''%%getline%%'' de la classe ''%%std::basic_istream%%''.
En fait, dans le code précédent, nous constatons un mélange entre le code qui implante et manipule le buffer
et le code qui lit les informations du flux stream
et qui analyse le contenu de la ligne. Il serait bien de séparer ces codes, de manière à rendre la lecture plus simple.
Pour simplifier le code, nous aimerions pouvoir non pas directement manipuler le pointeur sur la mémoire, mais éventuellement un objet qui encapsulerait la mémoire, un peu comme std::array
encapsule la mémoire allouée dans un tableau de taille fixe.
En effet, si imaginons que buffer
n’est plus un std::unique_ptr
, mais un objet plus riche qui offre une méthode permettant de redimensionner la classe temporary_buffer
, nous pourrions réécrire le code comme suit :
while(!stream.eof() && !stream.fail()) { // Try to load the full line into the buffer. stream.getline(buffer.data(), buffer.size()); size_t number_of_available_chars = (size_t)stream.gcount(); while(stream.fail() && !stream.eof() && number_of_available_chars == buffer.size() - 1) { // Increase the buffer as long as it is required. buffer.increase_by(increment); stream.clear(); stream.getline( buffer.data() + number_of_available_chars, buffer.size() - number_of_available_chars); number_of_available_chars += (size_t)stream.gcount(); } // Test if the line matches the regular expressions and // retrieve the name of the variable and the associated value. std::match_results<iterator> match; if(std::regex_match(buffer.begin(), buffer.end(), match, match_variables)) { variables[match[1].str()] = match[2].str(); } }
Nous nous proposons donc de générer une classe temporary_buffer
qui serait à même de fournir les fonctionnalités.
Déterminer l’ensemble des informations dont la classe temporary_buffer
a besoin. À partir de ces informations, déterminer l’ensemble des champs nécessaires ainsi que l’ensemble des méthodes dont vous avez impérativement besoin pour que le code précédent fonctionne.
Proposez une implantation pour l’ensemble des champs et méthodes que vous avez identifiées dans la question précédente.
Réécrivez le code de la fonction de lecture en utilisant désormais la classe temporary_buffer
et non plus un pointeur à accès exclusif sur une zone mémoire.
Nous souhaitons transformer la classe temporary_buffer
en classe générique. Typiquement, la classe temporary_buffer
est une classe qui correspond à une containeur. Nous souhaitons donc qu’elle puisse être manipulée comme un containeur. Pour ce faire, il est nécessaire de s’assure que cette classe respecte les contraintes associées au containeurs tel que définis par la norme C++ named requirements: Container.
Établissez l’ensemble des méthodes que vous devez ajouter à votre classe.
Terminez l’implantation de l’ensemble des définitions de types et méthodes que vous venez d’ajouter à votre classe.