6 votes

Combinez des expressions régulières et des plages provoque des problèmes de mémoire

Je voulais construire une vue sur toutes les sous-correspondances de regex dans text. Voici deux façons de définir une telle vue :

    char const text[] = "Les adresses IP sont : 192.168.0.25 et 127.0.0.1";
    std::regex regex{R"((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))"};

    auto sub_matches_view = 
        std::ranges::subrange(
            std::cregex_iterator{std::ranges::begin(text), std::ranges::end(text), regex},
            std::cregex_iterator{}
        ) |
        std::views::join;

    auto sub_matches_sv_view = 
        std::ranges::subrange(
            std::cregex_iterator{std::ranges::begin(text), std::ranges::end(text), regex},
            std::cregex_iterator{}
        ) |
        std::views::join |
        std::views::transform([](std::csub_match const& sub_match) -> std::string_view { return {sub_match.first, sub_match.second}; });
  • La valeur de sub_matches_view est std::csub_match. Elle est créée en construisant d'abord une vue d'objets std::cmatch (via l'itérateur regex), et puisque chaque std::cmatch est une série d'objets std::csub_match, elle est aplatie avec std::views::join.
  • La valeur de sub_matches_sv_view est std::string_view. Elle est identique à sub_matches_view, sauf qu'elle enveloppe également chaque élément de sub_matches_view dans un std::string_view.

Voici un exemple d'utilisation des plages ci-dessus :

for(auto const& sub_match : sub_matches_view) {
    std::cout << std::string_view{sub_match.first, sub_match.second} << std::endl; // #1
}

for(auto const& sv : sub_matches_sv_view) {
    std::cout << sv << std::endl; // #2
}

La boucle #1 fonctionne sans problèmes - les résultats imprimés sont corrects. Cependant, la boucle #2 provoque des problèmes de heap-use-after-free selon l'Address Sanitizer. En fait, simplement parcourir sub_matches_sv_view sans accéder aux éléments cause également ce problème. Ici se trouve le code sur Compiler Explorer ainsi que le résultat de l'Address Sanitizer.

Je suis à court d'idées quant à l'endroit où se situe mon erreur. text et regex ne deviennent jamais hors de portée, je ne vois aucun itérateur qui pourrait être accédé en dehors de leur durée de vie. L'objet std::csub_match contient des itérateurs (.first, .second) dans text, donc je ne pense pas qu'il doive rester en vie lui-même après la construction du std::string_view dans std::views::transform.

Je sais qu'il existe de nombreuses autres façons d'itérer sur les correspondances regex, mais je suis spécifiquement intéressé par ce qui cause les bugs de mémoire dans mon programme, je n'ai pas besoin de contournements pour ce problème.

8voto

Barry Points 45207

Le problème est std::regex_iterator et le fait qu'il stocke.


Ce type ressemble essentiellement à ceci :

class regex_iterator {
    vector matches;

public:
    auto operator*() const -> vector const& { return matches; }
};

Ce que cela signifie, par exemple, c'est que même si le type de référence de cet itérateur est T const&, si vous avez deux copies du même itérateur, elles vous donneront en fait des références vers des objets différents.

Maintenant, join_view::iterator ressemble essentiellement à ceci :

class iterator {
    // l'itérateur dans la plage que nous joignons
    iterator_t outer;

    // un itérateur dans *outer sur lequel nous itérons
    iterator_t> inner;
};

Ce qui, pour regex_iterator, ressemble donc à ceci :

class iterator {
    // les correspondances de regex
    vector outer;

    // la correspondance actuelle
    match* inner;
};

Maintenant, que se passe-t-il lorsque vous copiez cet itérateur ? L'élément inner de la copie fait toujours référence à l'élément outer de l'original ! Ces éléments ne sont pas réellement indépendants de la manière dont vous vous y attendriez. Ce qui signifie que si l'original se retrouve hors de portée, nous avons un itérateur pendante !

C'est ce que vous voyez ici : transform_view finit par copier l'itérateur (ce qui lui est certainement autorisé de faire), et maintenant vous avez un itérateur pendante (l'implémentation de libc++ effectue des déplacements, c'est pourquoi cela fonctionne dans ce cas comme l'a souligné). Mais nous pouvons reproduire le même problème sans transform tant que nous copions l'itérateur et détruisons l'original. Par exemple :

#include 
#include 
#include 
#include 

int main() {
    std::string_view text = "Les adresses IP sont : 192.168.0.25 et 127.0.0.1";
    std::regex regex{R"((\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3}))"};

    auto a =  std::ranges::subrange(
            std::cregex_iterator(std::ranges::begin(text), std::ranges::end(text), regex),
            std::cregex_iterator{}
        );

    auto b = a | std::views::join;

    std::optional i = b.begin();
    std::cout << std::string_view((*i)->first, (*i)->second) << '\n'; // bien

    auto j = *i;
    i.reset();
    std::cout << std::string_view(j->first, j->second) << '\n'; // boom
}

Je ne suis pas sûr de ce à quoi ressemblerait une solution à ce problème, mais la cause est le std::regex_iterator et non le views::join ou le views::transform.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X