J'aimerais supprimer certains éléments d'un conteneur ... Le problème est que je ne sais pas de quel type de conteneur il s'agit ... La plupart des algorithmes STL ne s'intéressent pas au conteneur: par exemple, find_if
, copy_if
, etc. tous fonctionnent plus ou moins avec n'importe quel type de conteneur.
Mais qu'en est-il de la suppression? Pour les conteneurs de type vector
, il existe l'idiome remove-erase-, auquel on ne peut toutefois pas s'appliquer, par exemple, des conteneurs de type set
-... se spécialiser pour des conteneurs particuliers, mais cela ne s'adapte pas lorsque d'autres conteneurs (unordered_set
, list
, ...) doivent également être pris en compte.
Ma question est la suivante: Comment implémenter une fonction qui supprime certains éléments de n’importe quel conteneur efficacement? Signature préférée:
template<typename Ts, typename Predicate>
void remove_if(Ts& ts, const Predicate& p);
Ou plus concrètement: comment distinguer les conteneurs de type set
- (insertion/suppression rapide, pas de commande personnalisée) des conteneurs de type vector
- (insertion lente/suppression, ordre personnalisé)? Existe-t-il un conteneur (couramment utilisé) qui ne rentre dans aucune de ces catégories?
Edit: Je viens de trouver std::experimental::erase_if
, qui contient des surcharges pour de nombreux conteneurs (tous?). Autrement dit, je n’accepterai une solution que si elle n’utilise pas std::experimental
.
Modifier:
Comme a noté par @pasbi, il semble que nous ayons déjà std::experimental::erase_if
, qui fait exactement cela! Il sera fusionné dans std::
en C++ 20.
Si vous voulez une implémentation personnalisée, lisez à l'avance.
Vous n'êtes pas obligé de vous spécialiser pour des conteneurs spécifiques. Au lieu de cela, vous pouvez utiliser les caractères de type et SFINAE pour déterminer la «catégorie» du conteneur.
Existe-t-il un conteneur (couramment utilisé) qui ne rentre dans aucune de ces catégories?
Je dirais oui. Il y a std::list
et std::forward_list
qui ont la fonction membre .remove_if()
, ce qui devrait être plus rapide que l'effacement-supprimer.
Ainsi, nous avons trois implémentations possibles:
Nous utilisons .remove_if()
si elle est disponible (comme déterminé par std::experimental::is_detected
).
De cette façon, nous gérons std::list
et std::forward_list
.
Sinon, nous utilisons effacer-supprimer si possible. (Ce qui est possible si les éléments de conteneur sont assignables par déplacement, ce qui peut être testé avec std::is_move_assignable
.)
Ainsi, nous traitons tous les conteneurs standard restants à l'exception de std::[unordered_]map
et std::[unordered_]set
. (C’est ce que vous appelez des conteneurs de type vector
-.)
Sinon, nous avons recours à une simple boucle d'effacement des éléments.
De cette façon, nous gérons std::[unordered_]map
et std::[unordered_]set
.
Exemple d'implémentation:
#include <algorithm>
#include <iterator>
#include <experimental/type_traits>
#include <utility>
inline auto dummy_predicate = [](auto &&){return false;};
template <typename T> using detect_member_remove_if =
decltype(std::declval<T&>().remove_if(dummy_predicate));
template <typename T, typename F> void remove_if(T &container, F &&func)
{
using element_t = std::remove_reference_t<decltype(*std::begin(container))>;
if constexpr (std::experimental::is_detected_v<detect_member_remove_if, T>)
{
container.remove_if(std::forward<F>(func));
}
else if constexpr (std::is_move_assignable_v<element_t>)
{
auto new_end = std::remove_if(std::begin(container), std::end(container),
std::forward<F>(func));
container.erase(new_end, std::end(container));
}
else
{
auto it = std::begin(container);
while (it != std::end(container))
{
if (func(*it))
it = container.erase(it);
else
it++;
}
}
}
Je préférerais quelque chose sans
experimental
Voici un remplacement personnalisé pour std::experimental::is_detected_v
:
namespace impl
{
template <typename ...P> struct void_impl {using type = void;};
template <typename ...P> using void_t = typename void_impl<P...>::type;
template <typename Dummy, template <typename...> typename A, typename ...B>
struct is_detected : std::false_type {};
template <template <typename...> typename A, typename ...B>
struct is_detected<void_t<A<B...>>, A, B...> : std::true_type {};
}
template <template <typename...> typename A, typename ...B>
inline constexpr bool is_detected_v = impl::is_detected<void, A, B...>::value;
Notez que nous n'utilisons pas C++ 17 std::void_t
car, autant que je sache, SFINAE ne fonctionne toujours pas correctement dans Clang.
std::erase
et std::erase_if
feront partie de C++ 20. Voir P1209
Libc ++ (trunk) implémente déjà cela (depuis hier :-)), et cela fera partie de clang 8.0.