Como remover vários elementos de um vetor em C++ – remove-erase idiom

remove-erase idiom em C++

Você já precisou remover vários elementos de um vetor de uma só vez em C++? Melhor ainda: você já tentou remover vários elementos de um vetor em C++ que satisfazem alguma condição (ou que não a satisfazem)? Se sim, seu primeiro reflexo deve ter sido percorrer todos os elementos do vetor, testar cada um deles e remover aqueles que falharam ou passam no teste, não? E se eu te disser que há uma forma mais eficiente e elegante de fazer a mesma coisa? Apresento-te o remove-erase idiom (ou idioma remove-erase, em português).

Remover elementos à moda antiga

Para entendermos como funciona o remove-erase, eu acredito ser boa idéia rever o modo “clássico” de fazer a mesma coisa; isto é: percorrendo o vetor e removendo os elementos desejados um a um. Vejamos como isso se dá.

No exemplo abaixo (o código também está disponível no meu perfil do Github) eu criei um vetor com os números inteiros de 1 a 10 (inclusivo), e eu decidi que queria remover apenas os números pares desse vetor, ou seja, 2, 4, 6, 8 e 10. Logo após o código há a explicação de como ele funciona.


Exemplo 1 – removendo vários elementos de um vetor manualmente

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector<int> myIntVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

  auto it = myIntVec.begin();

  // Remocao dos pares do vetor
  while (it != myIntVec.cend()) {
    if (*it % 2 == 0) {
      it = myIntVec.erase(it);
    } else
      ++it;
  }

  std::cout << "Conteudo do vetor apos a remocao: { ";
  for (auto num : myIntVec) {
    std::cout << num << " ";
  }
  std::cout << "}" << std::endl;
  return 0;
}

Conteudo do vetor apos a remocao: { 1 3 5 7 9 }


Antes de tudo, eu criei o vetor chamado myVec para armazenar os números inteiros de 1 a 10. Em seguida, criei um iterador para o início do vetor e chamei-o de it (se não conhecer os iteradores, recomendo que visite meu artigo sobre o assunto e depois volte aqui). Com o iterador em mãos, percorri o vetor verificando se o elemento em questão era par com a seguinte expressão: *it % 2 == 0, que significa “o resto da divisão do elemento para o qual aponta o iterador é zero?” e indica que o número é par. Ao encontrar um número par, eu o removi com o método erase da classe vector da biblioteca padrão. O erase remove o elemento do vetor para o qual aponta o iterador em questão (nesse caso, minha variável it) e retorna um iterador para o elemento que está logo após aquele que fora removido. Se o número não for par, apenas incremento o iterador para processar o próximo número do vetor.

Por fim, os elementos restantes do vetor são imprimidos na saída do programa. Como podemos ver, o vetor resultante contém apenas os números ímpares de 1 a 10.

Vejamos agora como funciona a mesma operação, mas dessa vez usando o remove-erase idiom.

Remover elementos com o remove-erase idiom

Para explicar como funciona o remove-erase idiom (eu tenho outro artigo no qual uso essa técnica, mas a explicação lá é mais breve: Como remover um elemento de um vetor em C++ – 6 métodos com exemplos), irei usar o código do exemplo 2 por base (o mesmo código está disponível no meu Github).


Exemplo 2 – removendo vários elementos de um vetor com remove-erase idiom

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
  std::vector<int> myIntVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

  // Remocao dos pares do vetor com a tecnica do remove-erase
  myIntVec.erase(std::remove_if(myIntVec.begin(), myIntVec.end(),
                                [](int num) { return (num % 2 == 0); }),
                 myIntVec.end());

  std::cout << "Conteudo do vetor apos a remocao: { ";
  for (auto num : myIntVec) {
    std::cout << num << " ";
  }
  std::cout << "}" << std::endl;
  return 0;
}

Conteudo do vetor apos a remocao: { 1 3 5 7 9 }


Para começar, precisamos do mesmo vetor com os dez elementos de 1 a 10. Como antes, chamei-o de myVec. Aí é que a coisa muda: não preciso do iterador it como antes, começo com o std::remove_if, que recebe três parâmetros: 1) um iterador para o início da faixa de valores que se deseja processar; 2) outro iterador para o fim dessa mesma faixa, e 3) uma função que retorna true para os elementos que se deseja remover do vetor. Tá bom, mas como funciona o remove_if?

função remove_if – funcionamento

O remove_if percore todos os elementos da faixa de valores em questão (delimitada pelos argumentos número 1 e 2 citados acima, os iteradores de início e fim da faixa), e utiliza cada um deles como argumento da função que lhe fora passada como terceiro argumento. Em seguida, cada um dos elementos que deva ser removido do vetor é deslocado para o final, deixando no início do vetor apenas aqueles elementos que permanecerão no vetor ao fim do remove_if.

O valor de retorno do remove_if é um iterador que aponta para o início da faixa de valores a ser removida do vetor. Com esse iterador de retorno, podemos invocar a função erase que recebe dois iteradores delimitantes da faixa dos elementos a ser removidos do vetor e os remove de fato.

função erase – funcionamento

A função erase é explicada em detalhes no mesmo artigo que citei acima (Como remover um elemento de um vetor em C++ – 6 métodos com exemplos), mas de forma resumida a versão que utilizei no exemplo 2 recebe dois iteradores: o primeiro, que delimita o início da faixa na qual os elementos do vetor devem ser removidos, e o segundo, que delimita o fim da faixa de modo excludente (esse iterador não é incluído naqueles que serão removidos, ele deve estar uma posição à frente do último elemento que se deseja remover).

Essa função não retorna nada, mas remove os elementos contidos na faixa delimitada pelos iteradores que lhe foram fornecidos como argumentos.

Funcionamento do remove-erase idiom

Agora que vimos como funcionam as partes do remove-erase idiom, falarei como funciona o conjunto inteiro.

Primeiro, a função remove_if “empurra” os elementos do vetor em questão que queremos remover para o final do vetor. Do retorno dessa função, então, obtemos um iterador que indica onde começa a zona dos elementos “rejeitados” (aqueles a ser removidos).

Em seguida, passamos esse último iterador para a função erase, e fornecemos como segundo argumento o iterador end() do vetor – um iterador que apontar para uma posição além do último elemento do vetor. Dessa forma, todos os elementos da zona de exclusão são removidos do vetor e no fim mantemos apenas aqueles elementos que desejamos no vetor.

⚠️🤓 Se quiser saber ainda mais detalhes sobre o remove-erase idiom, veja o Capítulo 20, páginas 743-744 do livro Professional C++, de Marc Gregoire. Lá há uma explicação bem detalhada dessa técnica.

Bônus: função lambda

Talvez você tenha notado algo estranho na versão do código que usa o padrão de design remove-erase idiom: onde está a função que falei que ia passar como terceiro argumento da função remove_if? Ela está lá, mas ela não tem nome – coloquei-a isolada no bloco de código abaixo. Ela é uma função lambda, que nada mais é que uma função sem nome. Fora isso, uma função lambda se comporta como uma função normal: pode receber argumento de entrada, retornar valores etc. Contudo, tais funções podem fazer coisas que funções normais não podem, como capturar variáveis do escopo do qual elas pertencem, como se ela possuísse “argumentos extras”, ou o super-poder de ver as variáveis que existem ao seu redor e não apenas aquelas que estão dentro dela. Legal, não? Eu farei um artigo dedicado a esse assunto, mas por enquanto saiba que tais funções existem e são muitíssimo úteis.

// Isto é uma função lambda com um parâmetro de entrada
// de tipo inteiro que se chama num. Ela retorna um booleano.
[](int num) { return (num % 2 == 0); }

Conclusão

Neste artigo vimos como remover vários elementos de um vetor usando o remove-erase idiom, ao invés de removê-los “na mão”. Mostrei a diferença entre os dois métodos, e falei das vantagens de se usar o remove-erase idiom. Expliquei também rapidamente as funções remove_if e erase da biblioteca padrão, e ainda discuti rapidamente as funções lambda.

Gostou do artigo? Então inscreva-se na nossa newsletter para não perder nenhum dos nossos artigos 🙂

Foto de perfil de Emanoel

Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.

Leave a Reply

Your email address will not be published. Required fields are marked *