web-dev-qa-db-fra.com

Sélectionner des éléments spécifiques à partir d'un vecteur

J'ai un vecteur v1 et un vecteur booléen v2 de la même taille. Je veux supprimer de v1 toutes les valeurs telles que l'élément parallèle de v2 est false:

vector<int> v3; // assume v1 is vector<int>
for (size_t i=0; i<v1.size(); i++)
    if (v2[i])
        v3.Push_back(v1[i]);
v1=v3;

Y a-t-il une meilleure façon de le faire?

  • en C++ 03
  • en C++ 11
19
user31264
size_t last = 0;
for (size_t i = 0; i < v1.size(); i++) {
  if (v2[i]) {
    v1[last++] = v1[i];
  }
}
v1.erase(v1.begin() + last, v1.end());

Identique au vôtre essentiellement, sauf que cela fonctionne sur place, ne nécessitant pas de stockage supplémentaire. Il s'agit essentiellement d'une réimplémentation de std::remove_if (qui serait difficile à utiliser directement, car l'objet de fonction utilisé utilise une valeur, et non un index ou un itérateur dans le conteneur).

20
Igor Tandetnik

En C++ 11, vous pouvez utiliser std::remove_if et std::erase avec un lambda, qui est le "erase-remove-idiom" :

size_t idx = 0;
v1.erase(std::remove_if(v1.begin(),
                          v1.end(),
                          [&idx, &v2](int val){return !v2[idx++];}),
           v1.end())

Et voici un lien qui fonctionne comme prévu: cpp.sh/57jpc

Comme le soulignent les commentaires, il y a un peu de discussion sur la sécurité de le faire de cette façon; L'hypothèse sous-jacente est que std::remove_if appliquera le prédicat aux éléments de v1in. Cependant, le langage de la doc ne le garantit pas explicitement. C'est simplement états :

La suppression est effectuée en décalant (au moyen d'une affectation de déplacement) les éléments de la plage de manière à ce que les éléments à ne pas supprimer apparaissent au début de la plage. L'ordre relatif des éléments restants est préservé et la taille physique du conteneur reste inchangée. Les itérateurs pointant vers un élément situé entre la nouvelle fin logique et la fin physique de la plage sont toujours déréférencables, mais les éléments eux-mêmes ont des valeurs non spécifiées (selon la post-condition MoveAssignable). Un appel à supprimer est généralement suivi d'un appel à la méthode d'effacement d'un conteneur, ce qui efface les valeurs non spécifiées et réduit la taille physique du conteneur afin qu'elle corresponde à sa nouvelle taille logique.

Maintenant, il serait difficile avec seulement un itérateur forward à un std::vector de garantir à la fois la stabilité des résultats et de ne pas appliquer le prédicat dans l'ordre. Mais il est certainement possible de le faire.

17
aruisdante

Une alternative basée sur remove_if est:

v1.erase(std::remove_if(v1.begin(), v1.end(),
                        [&v1, &v2](const int &x){ return !v2[&x - &v1[0]]; }),
         v1.end());

Notez également que si vous avez uniquement besoin d'une vue sur v1 dans laquelle certains éléments sont ignorés, vous pouvez éviter de modifier v1 et d'utiliser quelque chose comme boost::filter_iterator .

7
manlio

Je vous entends comme des lambdas. 

auto with_index_into = [](auto&v){
  return [&](auto&& f){
    return [&,f=decltype(f)(f)](auto& e){
      return f( std::addressof(e)-v.data(), e );
    };
  };
};

Cela peut être utile. Il prend un conteneur suporting .data(), puis renvoie un lambda de type ((Index,E&)->X)->(E&->X) - le lambda renvoyé convertit un visiteur d'élément indexé en visiteur d'élément. Une sorte de judo lambda.

template<class C, class Test>
auto erase_if( C& c, Test&& test) {
  using std::begin; using std::end;
  auto it=std::remove_if(begin(c),end(c),test);
  if (it==end(c)) return false;
  c.erase(it, end(c));
  return true;
}

parce que je déteste le supprimer supprimer idiome dans le code client.

Maintenant le code est joli:

erase_if( v1, with_index_into(v1)(
  [](std::size_t i, auto&e){
    return !v2[i];
  }
));

La restriction sur les déplacements dans supprimer/effacer devrait signifie qu'il appelle le lambda sur l'élément dans sa position d'origine.


Nous pouvons le faire avec des étapes plus élémentaires. Ça se complique au milieu ...

Tout d'abord, la petite bibliothèque d'opérateurs nommée:

namespace named_operator {
  template<class D>struct make_operator{};

  enum class lhs_token {
    star = '*',
    non_char_tokens_start = (unsigned char)-1,
    arrow_star,
  };

  template<class T, lhs_token, class O> struct half_apply { T&& lhs; };

  template<class Lhs, class Op>
  half_apply<Lhs, lhs_token::star, Op>
  operator*( Lhs&& lhs, make_operator<Op> ) {
    return {std::forward<Lhs>(lhs)};
  }
  template<class Lhs, class Op>
  half_apply<Lhs, lhs_token::arrow_star, Op>
  operator->*( Lhs&& lhs, make_operator<Op> ) {
    return {std::forward<Lhs>(lhs)};
  }

  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, lhs_token::star, Op>&& lhs, Rhs&& rhs )
  {
    return named_invoke( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }

  template<class Lhs, class Op, class Rhs>
  auto operator*( half_apply<Lhs, lhs_token::arrow_star, Op>&& lhs, Rhs&& rhs )
  {
    return named_next( std::forward<Lhs>(lhs.lhs), Op{}, std::forward<Rhs>(rhs) );
  }
}

Maintenant nous définissons then:

namespace lambda_then {
  struct then_t:named_operator::make_operator<then_t> {} then;

  template<class Lhs, class Rhs>
  auto named_next( Lhs&& lhs, then_t, Rhs&& rhs ) {
    return
      [lhs=std::forward<Lhs>(lhs), rhs=std::forward<Rhs>(rhs)]
      (auto&&...args)->decltype(auto)
    {
      return rhs( lhs( decltype(args)(args)... ) );
    };
  }
}
using lambda_then::then;

qui définit un jeton then tel que lambda1 ->*then* lambda2 renvoie un objet fonction prenant ses arguments, le transmet à lambda1, puis transmet la valeur de retour à lambda2.

Ensuite nous définissons to_index(container):

template<class C>
auto index_in( C& c ) {
  return [&](auto& e){
    return std::addressof(e)-c.data();
  };
}

nous conservons également le erase_if ci-dessus.

Cela se traduit par:

erase_if( v1,
  index_in(v1)
  ->*then*
  [&](auto i){
    return !v2[i];
  }
);

résoudre votre problème (exemple en direct).

7

En fait, j'aime bien la façon dont vous l'avez fait mais je ferais quelques modifications pour limiter la portée du vecteur temporaire et j'utiliserais std :: vector :: swap pour éviter une copie à la fin. Si vous avez C++11, vous pouvez utiliser std :: move au lieu de std :: vector :: swap :

#include <vector>
#include <iostream>

int main()
{
    std::vector<int> iv = {0, 1, 2, 3, 4, 5, 6};
    std::vector<bool> bv = {true, true, false, true, false, false, true};

    // start a new scope to limit
    // the lifespan of the temporary vector
    {
        std::vector<int> v;

        // reserve space for performance gains
        // if you don't mind an over-allocated return
        // v.reserve(iv); 

        for(std::size_t i = 0; i < iv.size(); ++i)
            if(bv[i])
                v.Push_back(iv[i]);

        iv.swap(v); // faster than a copy
    }

    for(auto i: iv)
        std::cout << i << ' ';
    std::cout << '\n';
}
3
Galik

Une version différente qui efface les éléments en place, mais ne nécessite pas autant de déplacements que celui demandé par Igor, et en cas de faible quantité d'éléments à effacer pourrait être plus efficace:

using std::swap;
size_t last = v1.size();
for (size_t i = 0; i < last;) {
   if( !v2[i] ) {
       --last;
       swap( v2[i], v2[last] );
       swap( v1[i], v1[last] );
   } else 
       ++i;
}
v1.erase(v1.begin() + last, v1.end());

mais cet algo est instable.

2
Slava

Si vous utilisez une list (ou forward_list pour C++ 11) au lieu d'une vector, vous pouvez le faire sur place sans la surcharge de déplacement/affectation/copie requise pour les opérations vector. Il est parfaitement possible de réaliser la plupart des tâches liées au stockage avec n’importe quel conteneur STL, mais le choix approprié des conteneurs donnera souvent des améliorations significatives en termes de performances.

1
Graham