Windows com C++

Usando expressões regulares com o C++ moderno

Kenny Kerr

Kenny Kerr
“C++ é uma linguagem para desenvolver e usar abstrações elegantes e eficientes.” — Bjarne Stroustrup

Esta citação do criador do C++ realmente resume o que eu amo na linguagem. Eu consigo desenvolver soluções elegantes para os meus problemas combinando os recursos da linguagem e os estilos de programação que considero mais adequados para a tarefa.

O C++11 introduziu uma longa lista de recursos que, por si só, são muito fascinantes, mas se tudo que você vê é uma lista de recursos isolados, não sabe o que está perdendo. A combinação desses recursos torna o C++ a potência que muitos estão aprendendo a apreciar. Vou ilustrar esse ponto mostrando-lhe como usar expressões regulares em C++ moderno. O padrão C + +11 introduziu uma poderosa biblioteca de expressões regulares, mas se você usá-la isoladamente – usando o estilo tradicional de programação um C++ – poderá achar um pouco cansativo. Infelizmente, essa é a maneira com que a maioria das bibliotecas C++ 11 tende a ser introduzida. Contudo, há certo mérito nessa abordagem. Se você estivesse procurando um exemplo conciso do uso de uma nova biblioteca, seria bem incômodo ser forçado a compreender uma série de novos recursos de linguagem ao mesmo tempo. Ainda assim, a combinação da linguagem C++ e recursos de biblioteca realmente transforma C++ em uma linguagem de programação produtiva.

Para manter os exemplos focados em C++ e não em expressões regulares, usarei necessariamente padrões muito simplistas. Você pode se perguntar por que estou usando expressões regulares para esses problemas triviais, mas isso ajuda a não me perder na mecânica de processamento de expressões. Este é um exemplo simples: eu gostaria de fazer a correspondência de sequências de caracteres de nomes, onde os nomes podem ser formatados "Kenny Kerr" ou "Kerr, Kenny". Preciso identificar o nome e o sobrenome e, em seguida, imprimi-los de alguma maneira consistente. A primeira é a sequência de caracteres de destino:

char const s[] = "Kerr, Kenny";

Para simplificar as coisas, continuarei com as sequências de caracteres char e evitarei usar a classe basic_string da biblioteca padrão, exceto para ilustrar os resultados de certas correspondências. Não há nada de errado com a basic_string, mas acho que a maior parte do trabalho que faço com expressões regulares tende a ser alvo de arquivos mapeados na memória. Copiar o conteúdo desses arquivos para objetos string serviria apenas para deixar meus aplicativos mais lentos. Suporte a expressões regulares da biblioteca padrão é indiferente e perfeitamente disposto em processar sequências de caracteres sem a preocupação de como elas são gerenciadas.

A próxima coisa que você precisa é de um objeto match:

auto m = cmatch {};

Essa é realmente uma coleção de objetos match. O cmatch é um modelo de classe match_results especializado em sequências de caracteres char. Nesse ponto, a “coleção” de objetos match está vazia:

ASSERT(m.empty());

Também preciso de um par de sequências de caracteres para receber os resultados:

string name, family;

Agora posso chamar a função regex_match:

if (regex_match(s, m, regex { R"((\w+) (\w+))" }))
{
}

Essa função tenta corresponder o padrão com a sequência inteira de caracteres. Isso está em contraste com a função regex_search que é muito disposta a procurar uma correspondência em qualquer ponto da sequência. Estou apenas criando o objeto regex "embutido" para abreviar, mas isso não é sem custo. Se você fosse corresponder essa expressão regular repetidamente, seria melhor criar o objeto regex uma vez e depois mantê-lo por toda a vida útil do aplicativo. O padrão anterior faz a correspondência dos nomes usando o formato "Kenny Kerr". Supondo que isso seja uma correspondência, posso simplesmente copiar as subsequências de caracteres:

name   = m[1].str();
family = m[2].str();

O operador subscrito retorna o objeto sub_match especificado. Um índice igual a zero representa a correspondência como um todo, enquanto índices subsequentes identificam os grupos identificados na expressão regular. Nem o objeto match_results nem o sub_match criará ou atribuirá uma subsequência de caracteres. Em vez disso, eles delineiam o intervalo de caracteres com um ponteiro ou iterador no início e no fim da correspondência ou subcorrespondência, produzindo o intervalo semiaberto habitual favorecido pela biblioteca padrão. Nesse caso, estou explicitamente chamando o método str em cada sub_match para criar uma cópia de cada subcorrespondência como objetos string.

Isso manipula o primeiro formato possível. Para o segundo, preciso de outra chamada para regex_match com o padrão alternativo (tecnicamente, você poderia combinar ambos os formatos com uma única expressão, mas isso não vem ao caso):

else if (regex_match(s, m, regex { R"((\w+), (\w+))" }))
{
  name   = m[2].str();
  family = m[1].str();
}

Esse padrão faz a correspondência dos nomes usando o formato "Kerr, Kenny". Repare que eu tive de inverter os índices, pois o primeiro grupo representado nesta expressão regular identifica o sobrenome, enquanto o segundo identifica o nome. Isso é pertinente à função regex_match. A Figura 1 mostra a listagem completa para referência.

Figura 1 O exemplo de referência regex_match

char const s[] = "Kerr, Kenny";
auto m = cmatch {};
string name, family;
if (regex_match(s, m, regex { R"((\w+) (\w+))" }))
{
  name   = m[1].str();
  family = m[2].str();
}
else if (regex_match(s, m, regex { R"((\w+), (\w+))" }))
{
  name   = m[2].str();
  family = m[1].str();
}
else
{
  printf("No match\n");
}

Não sei o que você, mas o código na Figura 1 parece maçante para mim. Embora a biblioteca de expressões regulares seja certamente poderosa e flexível, ela não é particularmente elegante. Preciso saber sobre os objetos match_results e sub_match. Preciso lembrar como essa "coleção" é indexada e como extrair os resultados. Eu poderia evitar fazer as cópias, mas rapidamente isso se torna oneroso.

Eu já usei uma série de novos recursos linguagem C + + que você pode ou não ter encontrado antes, mas nada deve ser excessivamente surpreendente. Agora quero mostrar como você pode usar modelos variadic para realmente dar uma apimentada no uso de expressões regulares. Em vez de mergulhar de cabeça em mais recursos de linguagem, vou começar mostrando uma abstração simples para simplificar o processamento de texto para que eu possa manter a praticidade e elegância.

Primeiramente, definirei um tipo simples para representar uma sequência de caracteres que não são necessariamente de terminação nula. Esta é a classe strip:

struct strip
{
  char const * first;
  char const * last;
    strip(char const * const begin,
          char const * const end) :
      first { begin },
      last  { end }
    {}
    strip() : strip { nullptr, nullptr } {}
};

Há, sem dúvida, muitas dessas classes que eu possa reutilizar, mas acho que isso ajuda a evitar muitas dependências ao produzir abstrações simples.

A classe strip não faz muita coisa, mas vou incrementá-la com um conjunto de funções não membros. Vou começar com duas funções para definir o intervalo genericamente:

auto begin(strip const & s) -> char const *
{
  return s.first;
}
auto end(strip const & s) -> char const *
{
  return s.last;
}

Embora não seja estritamente necessária para este exemplo, acho que esta técnica fornece uma medida de consistência válida com contêineres e algoritmos da biblioteca padrão. Vou voltar para as funções begin e end em breve. Em seguida, temos a função auxiliar make_strip:

template <unsigned Count>
auto make_strip(char const (&text)[Count]) -> strip
{
  return strip { text, text + Count - 1 };
}

Esta função é útil quando se tenta criar uma strip a partir de um literal de sequência de caracteres. Por exemplo, posso inicializar uma strip da seguinte forma:

auto s = make_strip("Kerr, Kenny");

Em seguida, é geralmente útil determinar o comprimento ou o tamanho da strip:

auto size(strip const & s) -> unsigned
{
  return end(s) - begin(s);
}

Aqui você pode ver que eu estou simplesmente reutilizando as funções begin e end para evitar uma dependência nos membros da strip. Eu poderia proteger os membros da classe strip. Por outro lado, muitas vezes é útil poder manipulá-los diretamente de um algoritmo. Ainda assim, se eu não precisar aceitar uma dependência difícil, não aceitarei.

Obviamente, é simples o bastante criar uma sequência de caracteres padrão de uma strip:

auto to_string(strip const & s) -> string
{
  return string { begin(s), end(s) };
}

Isso pode vir a calhar se alguns resultados sobreviverem às sequências de caracteres originais. Isso completa o manuseio básico da strip. Posso inicializar uma strip e determinar seu tamanho – e graças às funções begin e end, posso usar uma instrução range-for para iterar por seus caracteres:

auto s = make_strip("Kenny Kerr");
for (auto c : s)
{
  printf("%c\n", c);
}

Quando escrevi pela primeira vez a classe strip, eu esperava que poderia chamar seus membros "begin" e "end" em vez de "first" e "last". O problema é que o compilador, quando confrontado com uma instrução range-for, primeiro tenta encontrar os membros adequados que podem ser chamados de funções. Se o intervalo ou a sequência de destino não inclui membros chamados begin e end, o compilador procura um par adequado no escopo de delimitação. O problema é que, se o compilador encontrar membros chamados begin e end, mas eles não forem adequados, ele não tentará procurar mais. Isso pode parecer falta de visão, mas a linguagem C++ tem regras de pesquisa de nome complexas, e qualquer outra coisa tornaria isso ainda mais confuso e inconsistente.

A classe strip é uma construção pouco simples, mas não faz muito sozinha. Agora vou combiná-lo com a biblioteca de expressões regulares para produzir uma abstração elegante. Quero ocultar a mecânica do objeto match, a parte maçante do processamento de expressões. É aí que os modelos variadic entram. O segredo para compreender os modelos variadic é perceber que você pode separar o primeiro argumento dos demais. Isso normalmente resulta na recursão do tempo de compilação. Posso definir um modelo variadic para descompactar um objeto match nos argumentos subsequentes:

template <typename... Args>
auto unpack(cmatch const & m,
            Args & ... args) -> void
{
  unpack<sizeof...(Args)>(m, args...);
}

O “typename...” indica que Args é um pacote de parâmetros de modelo. O “...” correspondente no tipo de args indica que args é um pacote de parâmetros de função. A expressão “sizeof...” determina o número de elementos no pacote de parâmetros. O “...” final após args diz ao compilador para expandir o pacote de parâmetros em sua sequência de elementos.

O tipo de cada um argumento pode ser diferente, mas, neste caso, cada um será uma referência de strip sem const. Estou usando um modelo variadic para que haja suporte a um número desconhecido de argumentos. Até agora, a função unpack não parece ser recursiva. Ela encaminha seus argumentos para outra função unpack com um argumento de modelo adicional:

template <unsigned Total, typename... Args>
auto unpack(cmatch const & m,
            strip & s,
            Args & ... args) -> void
{
  auto const & v = m[Total - sizeof...(Args)];
  s = { v.first, v.second };
  unpack<Total>(m, args...);
}

No entanto, esta função unpack separa o primeiro argumento após o objeto match dos demais. Essa é a recursão do tempo de compilação em ação. Partindo do princípio de que o pacote de parâmetros args não está vazio, ele chama a si próprio com os demais argumentos. No final, a sequência de argumentos torna-se vazia e uma terceira função unpack é necessária para lidar com esta conclusão:

template <unsigned>
auto unpack(cmatch const &) -> void {}

Essa função não faz nada. Ela simplesmente reconhece o fato de que o pacote de parâmetros pode estar vazio. As funções unpack anteriores têm a chave para descompactar o objeto match. A primeira função unpack capturou o número original de elementos do pacote de parâmetros. Isso é necessário porque cada chamada recursiva efetivamente produzirá um novo pacote de parâmetros com um tamanho reduzido. Observe como estou subtraindo o tamanho do pacote de parâmetros do total original. Diante desse tamanho total ou estável, posso indexá-lo na coleção de objetos match para recuperar os subcorrespondentes individuais e copiar seus respectivos limites para os argumentos variadic.

Isso se encarrega de descompactar o objeto match. Embora não seja necessário, continuo a achar que é útil ocultar o objeto match propriamente dito se ele não for necessário diretamente – por exemplo, se for necessário apenas para acessar o prefixo e sufixo match. Eu vou encerrar tudo isso para fornecer uma abstração de match mais simples:

template <typename... Args>
auto match(strip const & s,
           regex const & r,
           Args & ... args) -> bool
{
  auto m = cmatch {};
  if (regex_match(begin(s), end(s), m, r))
  {
    unpack<sizeof...(Args)>(m, args...);
  }
    return !m.empty();
}

Esta função também é um modelo variadic mas não é propriamente recursiva. Ela meramente encaminha seus argumentos para a função unpack original para processamento. Ela também se encarrega de fornecer um objeto match local e definir a sequência de pesquisa em termos de funções auxiliares begin e end da strip. Uma função quase idêntica pode ser escrita para acomodar regex_search em vez de regex_match. Agora posso reescrever o exemplo da Figura 1 muito mais simples:

auto const s = make_strip("Kerr, Kenny");
strip name, family;
if (match(s, regex { R"((\w+) (\w+))"  }, name,   family) ||
    match(s, regex { R"((\w+), (\w+))" }, family, name))
{
  printf("Match!\n");
}

E quanto à iteração? As funções unpack também vêm a calhar para manipular os resultados da correspondência de uma pesquisa interativa. Imagine uma sequência de caracteres com o canônico "Olá, mundo" em uma variedade de idiomas:

auto const s =
  make_strip("Hello world/Hola mundo/Hallo wereld/Ciao mondo");

Posso fazer correspondência de cada um com a seguinte expressão regular:

auto const r = regex { R"((\w+) (\w+))" };

A biblioteca de expressões regulares fornece o regex_iterator para iterar entre as correspondências, mas usar iteradores diretamente pode ser maçante. Uma opção é escrever uma função for_each que chama um predicado para cada correspondência:

template <typename F>
auto for_each(strip const & s,
              regex const & r,
              F callback) -> void
{
  for (auto i = cregex_iterator { begin(s), end(s), r };
       i != cregex_iterator {};
       ++i)
  {
    callback(*i);
  }
}

Eu poderia, então, chamar esta função com uma expressão lambda para descompactar cada correspondência:

for_each(s, r, [] (cmatch const & m)
{
  strip hello, world;
  unpack(m, hello, world);
});

Isso certamente funciona, mas eu sempre acho que é frustrante não poder quebrar facilmente esse tipo de construção de loop. A instrução range-for fornece uma alternativa mais conveniente. Vou começar definindo um intervalo de iteradores simples que o compilador reconhecerá para implementar o loop range-for:

template <typename T>
struct iterator_range
{
  T first, last;
  auto begin() const -> T { return first; }
  auto end() const -> T { return last; }
};

Agora posso escrever uma função for_each mais simples que retorna apenas um iterator_range:

auto for_each(strip const & s,
              regex const & r) -> iterator_range<cregex_iterator>
{
  return
  {
    cregex_iterator { begin(s), end(s), r },
    cregex_iterator {}
  };
}

O compilador se encarregará de produzir a iteração e posso simplesmente escrever uma instrução range-for com sobrecarga sintática mínima, quebrando-a com antecedência se eu quiser:

for (auto const & m : for_each(s, r))
{
  strip hello, world;
  unpack(m, hello, world);
  printf("'%.*s' '%.*s'\n",
         size(hello), begin(hello),
         size(world), begin(world));
}

O console apresenta os resultados esperados:

'Hello' 'world'
'Hola' 'mundo'
'Hallo' 'wereld'
'Ciao' 'mondo'

C++11 e as linguagens posteriores oferecem a oportunidade para revitalizar o desenvolvimento de software em C++ com um estilo moderno de programação que lhe permite produzir abstrações elegantes e eficientes. A gramática de expressões regulares pode abater até o mais experiente dos desenvolvedores. Por que não passar alguns minutos desenvolvendo uma abstração mais elegante? Pelo menos a parte em C++ de sua tarefa será um prazer!

Kenny Kerr é programador de computador, autor da Pluralsight e Microsoft MVP que mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter em twitter.com/kennykerr.