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 find_all_variables(std::string filename)
{
size_t buffer_size = 1024;
using iterator = char*;
char* buffer = new char[1024];
std::map 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 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;
}
===== Question n°1.1 =====
Tester le code précédent ? Expliquer pourquoi le bloc ''%%try {} catch(...) {}%%'' a été introduit dans le code ?
size_t buffer_size = 1024;
using iterator = char*;
char* buffer = (char*)alloca(buffer_size);
std::ifstream stream(filename);
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 the
// variable definitions.
iterator current_iterator = buffer;
iterator end_iterator = buffer + buffer_size;
std::match_results 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;
}
}
return variables;
Cela effectuerait le travail. Cependant, cette technique n’est pas conseillée et elle correspond plus à un style de programmation C que C++. En plus, la taille de la pile n’est pas toujours extensible à souhait, cela dépend des processeurs. Ce type de code peut parfois conduire à la génération d’erreurs liées à un dépassement de la taille maximale de la pile, notamment sur des architectures embarquées où la taille maximale de la pile peut-être assez restreinte.
Comme la taille du buffer est une constante, il est possible d’allouer un tableau d’entier de taille statique [[https://en.cppreference.com/w/cpp/container/array|''%%std::array%%'']]. Un tableau d’entier de taille statique est a priori alloué sur la pile, cependant, si le tableau est trop grand, il pourra être alloué dans le tas. C’est l’implantation associée à la plateforme et au compilateur qui déterminera où la mémoire sera effectivement allouée.
Dans ce cas, la déclaration de ''%%buffer%%'' se réécrit en :
const size_t buffer_size = 1024;
using iterator = std::array::iterator;
std::array buffer;
Désormais, nous ne manipulons plus un pointeur, mais un containeur. Il fait pour ce faire modifier l’appel à la fonction ''%%read%%'' qui lit les données à partir du flux :
stream.read(buffer.data(), buffer.size());
De même, nous définissons les itérateurs en appelant directement les fonctions ''%%begin%%'' et ''%%end%%'' du containeur ''%%buffer%%''.
iterator current_iterator = buffer.begin();
iterator end_iterator = buffer.end();
std::match_results match;
while(current_iterator != end_iterator &&
std::regex_search(
current_iterator, end_iterator,
match, match_variables))
{
variables[match[0].str()] = match.length();
std::advance(current_iterator, match.length());
}
Ce qui nous donne le code consolidé suivant :
std::map find_all_variables(std::string filename)
{
const size_t buffer_size = 1024;
using iterator = std::array::iterator;
std::array buffer;
std::map variables;
std::ifstream stream(filename);
while(!stream.eof() && !stream.fail())
{
// Load the buffer
stream.read(buffer.data(), 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.begin();
iterator end_iterator = buffer.end();
std::match_results 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;
}
}
return variables;
}
size_t buffer_size = 80;
auto memory = std::make_unique(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(new_buffer_size);
std::copy(memory, memory + buffer_size, new_memory);
buffer_size = new_buffer_size;
std::swap(memory, new_memory);
}
===== Question n°2.1 =====
Expliquez le fonctionnement du code précédent. Pourquoi empêche-t-il d’avoir une fuite mémoire ?
stream.getline(buffer.get(), buffer_size);
size_t number_of_available_chars = (size_t)stream.gcount();
Si nous sommes dans un état d’erreur, nous répétons le processus suivant :
Nous augmentons la taille du buffer
Nous lisons les caractères suivant en appelant de nouveau la méthode ''%%getline%%''
Et ce tant que nous n’avons atteint ni la fin du flux ni la fin de la ligne.
while(stream.fail() && !stream.eof()
&& number_of_available_chars == buffer_size - 1)
{
// Increase the size of the buffer.
auto new_buffer_size = buffer_size + 40;
auto new_buffer = std::make_unique(new_buffer_size);
std::copy(buffer.get(),
buffer.get() + buffer_size, new_buffer.get());
// Load the upper part of the buffer.
stream.clear();
stream.getline(
new_buffer.get() + number_of_available_chars,
new_buffer_size - number_of_available_chars);
number_of_available_chars += (size_t)stream.gcount();
// Swap both buffers
buffer.swap(new_buffer);
buffer_size = new_buffer_size;
}
Ce qui nous donne le code consolidé suivant :
std::map find_all_variables(std::string filename)
{
size_t buffer_size = 80;
using iterator = char*;
auto buffer = std::make_unique(buffer_size);
std::map variables;
std::ifstream stream(filename);
while(!stream.eof() && !stream.fail())
{
// Load the buffer
stream.getline(buffer.get(), 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 size of the buffer.
auto new_buffer_size = buffer_size + 40;
auto new_buffer = std::make_unique(new_buffer_size);
std::copy(buffer.get(),
buffer.get() + buffer_size, new_buffer.get());
// Load the upper part of the buffer.
stream.clear();
stream.getline(
new_buffer.get() + number_of_available_chars,
new_buffer_size - number_of_available_chars);
number_of_available_chars += (size_t)stream.gcount();
// Swap both buffers
buffer.swap(new_buffer);
buffer_size = new_buffer_size;
}
// Test if the line that has been loaded denotes
// a variable definition.
std::match_results match;
if(std::regex_match(buffer.get(), buffer.get() + buffer_size,
match, match_variables))
{
variables[match[1].str()] = match[2].str();
}
}
return variables;
}
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 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.
==== Question n°3.1 ====
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.
template
class temporary_buffer
{
public:
using value_type = T;
using pointer = value_type*;
using iterator = value_type*;
using size_type = size_t;
private:
std::unique_ptr m_memory;
size_type m_size;
public:
explicit temporary_buffer(size_type initial_size);
~temporary_buffer();
constexpr iterator begin();
constexpr iterator end();
void increase_by(size_type number_of_elements);
constexpr pointer data() { return m_memory.get(); }
constexpr size_type size() const noexcept { return m_size; }
};
template
class temporary_buffer
{
public:
using value_type = T;
using pointer = value_type*;
using iterator = value_type*;
using size_type = size_t;
private:
std::unique_ptr m_memory;
size_type m_size;
public:
explicit temporary_buffer(
size_type initial_size):
m_size(initial_size),
m_memory(std::make_unique_for_overwrite(initial_size))
{}
~temporary_buffer() = default;
constexpr iterator begin() { return m_memory.get(); }
constexpr iterator end() { return m_memory.get() + m_size; }
constexpr pointer data() { return m_memory.get(); }
void increase_by(size_type number_of_elements)
{
size_type new_size = m_size + number_of_elements;
auto new_memory = std::make_unique_for_overwrite(new_size);
std::copy_n(m_memory.get(), m_size, new_memory.get());
m_size = new_size;
std::swap(m_memory, new_memory);
}
constexpr size_type size() const noexcept { return m_size; }
};
std::unique_ptr buffer = std::make_unique(buffer_size)
par :
buffer_type buffer(buffer_size) ;
Il faut de plus redéfinir le type itérateur :
using buffer_type = temporary_buffer;
using iterator = typename buffer_type::iterator;
De même les appels à la méthode ''%%getline%%'' sont modifiés en appelant non plus la méthode ''%%get()%%'' de ''%%std::unique_ptr%%'', mais la méthode ''%%data()%%'' de ''%%temporary_buffer%%''. Il en va de même pour la taille du buffer qui désormais est stockée dans l’objet ''%%buffer%%''et non plus dans la variable ''%%buffer_size%%''.
Pour exemple :
stream.getline(buffer.get(), buffer_size);
est réécrit en :
stream.getline(buffer.data(), buffer.size());
Enfin, le code correspond à redimensionnement du buffer doit être supprimé et est remplacé par le seul appel à la méthode ''%%increase_by%%'' de la classe ''%%temporary_buffer%%'' :
buffer.increase_by(increment);
Toutes ces modifications nous fournissent le code suivant :
std::map find_all_variables(std::string filename)
{
using buffer_type = temporary_buffer;
using iterator = typename buffer_type::iterator;
const size_t buffer_size = 80;
const size_t increment = 40;
buffer_type buffer(buffer_size) ;
std::map variables;
std::ifstream stream(filename);
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 match;
if(std::regex_match(buffer.begin(), buffer.end(),
match, match_variables))
{
variables[match[1].str()] = match[2].str();
}
}
return variables;
}
template
class temporary_buffer
{
private:
std::unique_ptr m_memory;
size_type m_size;
public:
using value_type = T;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = value_type*;
using const_pointer = const value_type*;
using iterator = value_type*;
using const_iterator = const T*;
using difference_type = ptrdiff_t;
using size_type = size_t;
temporary_buffer() noexcept;
temporary_buffer(const temporary_buffer& another_buffer);
temporary_buffer(temporary_buffer&& another_buffer);
~temporary_buffer() = default;
constexpr temporary_buffer& operator = (const temporary_buffer& another_buffer);
temporary_buffer& operator = (temporary_buffer&& another_buffer) noexcept;
constexpr iterator begin() noexcept;
constexpr const_iterator begin();
constexpr const_iterator cbegin();
constexpr const_iterator cend();
constexpr iterator end() noexcept;
constexpr const_iterator end();
constexpr bool empty() const noexcept;
void increase_by(size_type number_of_elements);
constexpr size_type max_size() const noexcept;
constexpr size_type size() const noexcept;
constexpr void swap(temporary_buffer& another_buffer);
constexpr bool operator == (const temporary_buffer& another_buffer) const noexcept;
};
constexpr iterator begin() noexcept { return m_memory.get(); }
constexpr const_iterator begin() const noexcept { return m_memory.get(); }
constexpr const_iterator cbegin() const noexcept { return m_memory.get(); }
constexpr const_iterator cend() const noexcept { return m_memory.get() + m_size; }
constexpr iterator end() { return m_memory.get() + m_size; }
constexpr const_iterator end() const noexcept { return m_memory.get() + m_size; }
Il en va de même pour les méthodes accédant aux nombres d’éléments et à la structure interne :
constexpr bool empty() const noexcept { return m_size == 0; }
constexpr pointer data() noexcept { return m_memory.get(); }
constexpr const_pointer data() const noexcept { return m_memory.get(); }
constexpr size_type max_size() const noexcept { return std::numeric_limits::max(); }
constexpr size_type size() const noexcept { return m_size; }
Cependant, il est intéressant de se pencher sur les constructeurs de recopie ainsi que les opérateurs d’affectation. Nous avons deux constructeurs de recopie et deux opérateurs d’affectation.
temporary_buffer(const temporary_buffer& another_buffer);
temporary_buffer(temporary_buffer&& another_buffer);
constexpr temporary_buffer& operator = (const temporary_buffer& another_buffer);
temporary_buffer& operator = (temporary_buffer&& another_buffer) noexcept;
Qu’est ce qui distingue l’opérateur :
constexpr temporary_buffer& operator = (const temporary_buffer& another_buffer);
de l’opérateur :
temporary_buffer& operator = (temporary_buffer&& another_buffer) noexcept;
En fait, si nous considérons le code suivant :
temporary_buffer create_buffer() { return temporary_buffer(10) }
auto buffer = create_buffer();
auto copy_of_buffer = buffer;
la fonction ''%%create_buffer%%'' va créer un buffer, ce buffer va être dupliqué dans le résultat de la fonction et il sera juste détruit après. De même le résultat de la fonction va être recopié dans la variable ''%%buffer%%'' et il va être ensuite détruit juste après l’affectation.
Par contre, la dernière ligne indique que ''%%copy_of_buffer%%'' est créé comme étant une copie de ''%%buffer%%'', mais que ''%%buffer%%'' continuera à être utilisé ensuite.
Il est possible de définir différentes sémantiques de recopie. En C++, il en existe deux. La première sémantique est la sémantique habituelle, aussi apellée **//copy value semantics//**, le contenu de l’objet destination reçoit une copie des informations définies dans l’objet source, le contenu de l’objet source est préservé. La seconde sémantique appelée **//move semantics//** par contre déplace le contenu de l’objet source vers le contenu de l’objet destination. Le contenu de l’objet source est supposé avoir été déplacé vers l’objet destination, il est soit invalidé, détruit ou réinitialisé.
Typiquement dans notre cas, lorsque nous dupliquons le buffer qui vient d’être crée dans la variable ''%%buffer%%'', nous devrions a priori créer une nouvelle zone mémoire et ensuite recopier les données de la zone mémoire initiale vers la zone mémoire nouvellement créée. Ceci est inefficace, sachant que l’objet ''%%temporary_buffer(10)%%'' ne sera jamais plus utilisé. Il serait bien plus opportun de déplacement le contenu de l’objet qui a été créé par l’appel du constructeur ''%%temporary_buffer(10)%%'' (et qui sera détruit au moment de sortir de la fonction ''%%create_buffer%%'') vers l’objet qui est créé et qui sera stocké dans la variable ''%%buffer%%''. Pour que le compilateur puisse optimiser le code, il faut expliquer comment procéder à la création ou à l’affectation d’un objet en effectuant non pas une recopie mais un transfert de recopie.
Le constructeur ou l’opérateur d’affectation suivant :
temporary_buffer(temporary_buffer&& another_buffer);
temporary_buffer& operator = (temporary_buffer&& another_buffer);
définissent le constructeur et l’opérateur d’affectation qui vont transférer le contenu du buffer ''%%another_buffer%%'' vers l’objet en cours de création ou l’objet destinataire de l’affectation. Comme le contenu de l’objet passé en paramètre ''%%temporary_buffer
temporary_buffer(temporary_buffer&& another_buffer) noexcept:
m_size(another_buffer.m_size),
m_memory(another_buffer.m_memory)
{
another_buffer.m_size = 0;
}
temporary_buffer& operator = (temporary_buffer&& another_buffer) noexcept
{
if(&another_buffer != this)
{
m_size = another_buffer.m_size;
m_memory = another_buffer.m_memory;
another_buffer.m_size = 0;
}
return *this;
}
En effet, la classe ''%%std::unique_ptr%%'' garantissant l’exclusivité ne supporte que la sémantique de transfert du contenu, l’instruction :
m_memory = another_buffer.m_memory;
transfère la référence de l’objet pointeur ''%%another_buffer.m_memory%%'' et remet le contenu de ce pointeur à ''%%std::null_ptr%%'' pour indiquer que le pointeur ne pointe pas sur une zone définie.
Cependant, si nous souhaitons dupliquer le contenu, il faut fournir un constructeur de copie ainsi qu’un opérateur d’affectation qui effectue non pas un transfert, mais une copie du contenu de l’objet source vers l’objet destination. Il s’agit des constructeurs et opérateurs de copie habituels de C++ :
temporary_buffer(const temporary_buffer& another_buffer);
temporary_buffer& operator = (const temporary_buffer& another_buffer);
Dans ce cas, il est nécessaire de créer une nouvelle zone mémoire qui va stocker les informations en provenance la zone mémoire à laquelle le buffer source fait référence et ensuite de copier le contenu de la zone mémoire initiale vers la zone mémoire destination.
temporary_buffer(const temporary_buffer& another_buffer):
m_size(another_buffer.m_size),
m_memory(std::make_unique_for_overwrite(m_size))
{
std::copy_n(another_buffer.data(), m_size, m_memory.get());
}
constexpr temporary_buffer& operator = (const temporary_buffer& another_buffer)
{
if(&another_buffer != this)
{
m_size = another_buffer.m_size;
m_memory(std::make_unique_for_overwrite(another_buffer.m_size)),
std::copy_n(another_buffer.data(), m_size, m_memory.get());
}
return *this;
}
Enfin, il ne reste plus qu’à définir l’opérateur de comparaison. Celui-ci se limite à comparer si les zones mémoires référencées sont égales :
constexpr bool operator == (const temporary_buffer& another_buffer) const noexcept
{
return m_memory == another_buffer.m_memory;
}
ainsi que la méthode ''%%swap()%%'' qui va permuter le contenu de chacun des objets buffers. Après cette opération, le buffer courant fera référence à la zone mémoire initialement référencée par le second buffer et le second buffer fera référence à la zone mémoire initialement référencée par le premier buffer.
constexpr void swap(temporary_buffer& another_buffer) noexcept
{
std::swap(m_memory, another_buffer.m_memory);
std::swap(m_size, another_buffer.m_size);
}
Pour être complètement conforme, il faut surcharger la fonction ''%%std::swap%%'' de la bibliothèque standard qui prend deux buffers en argument et en permute les contenus.
template
constexpr void swap(temporary_buffer& first_buffer, temporary_buffer& second_buffer)
{
first_buffer.swap(second_buffer);
}