[Actualité] Extraire des collections de données depuis une collection initiale
par
, 29/10/2020 à 18h16 (4893 Affichages)
Il est commun de devoir extraire des vector depuis un vector initial.
Si l'on veut extraire des vector d'utilisateurs par exemple, tout en s'assurant que chaque utilisateur n'est présent que dans un vector à la fois.
La première approche d'un tel problème ressemblerait probablement à ceci :
Le code est plutôt lisible, mais certains cas peuvent vite entraîner des conditions complexes à écrire.
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 std::vector<..> originalData; std::vector<...> list1; std::vector<...> list2; std::vector<...> list3; std::vector<...> list4; for (..& data : originalData) { if (...) list1.push_back(data); else if (...) list2.push_back(data); else if (...) list3.push_back(data); else list4.push_back(data); }
Toutefois, ce code va effectuer une copie de chaque donnée depuis le vector original vers le vector auquel elle appartient.
Pour supprimer une copie, les vector pourraient être des vector de pointeurs. Mais le vector lui-même réalisera toujours une allocation voire plusieurs réallocations selon la quantité de données.
Pourquoi faire des copies alors que toutes les données sont disponibles dans le vector initial ?
Pourquoi faire la moindre allocation quand les données existent déjà ?
Chaque liste est un sous-ensemble du vector original. Celui-ci possède toutes les données, et c'est très bien ainsi.
Pour y parvenir, on pourrait imaginer un std::sort qui réorganise ce vector original, mais là aussi les conditions peuvent vite devenir complexes à écrire.
Pour y parvenir relativement simplement, nous allons utilisons un petit truc peu connu : std::partition, ou std::stable_partition pour garder un éventuel ordre des données.
partition va réordonner les éléments du vector de telle sorte que tous ceux qui valident la condition (le prédicat retourne true), seront placés en tête de la collection, puis retournera un itérateur sur le premier élément pour lequel le prédicat retourne false.
Exemple :
Qui donnera le résultat suivant :
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 std::vector<int> tests{ 0,1,2,3,4 }; auto it_part = std::stable_partition(tests.begin(), tests.end(), [](int v) { return v < 3; }); std::cout << "Before part : "; for (auto it = tests.begin(); it != it_part; ++it) std::cout << *it << ","; std::cout << std::endl << "After part : "; for (auto it = it_part; it != tests.end(); ++it) std::cout << *it << ",";D'abord, il nous faut une petite structure qui permet de réaliser une vue sur un vector.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2 Before part : 0,1,2, After part : 3,4,
Il n'existe malheureusement pas (encore) de vector_view dans le standard. Il y a bien std::span si vous êtes en C++20, mais celui-ci refuse une syntaxe std::span<int> sp(values.begin(), values.end());, ce qui est plutôt dérangeant...
Voici la très simple structure que nous allons utiliser :
Elle contient le strict minimum nécessaire, et vous êtes bien sûr libres d'y ajouter ce dont vous avez besoin.
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 template <class T> class VectorView { public: VectorView() { // Default values to prevent iterating over random memory m_begin = m_end; } VectorView(typename std::vector<T>::const_iterator first, typename std::vector<T>::const_iterator end) : m_begin(first) , m_end(end) {} auto begin() const { return m_begin; } auto end() const { return m_end; } private: typename std::vector<T>::const_iterator m_begin, m_end; };
Pour l'exercice, nous allons trier des nombres.
Le but est de finir avec cette syntaxe :
Ce que j'estime être une façon plutôt claire et élégante d'extraire les données qui nous intéressent.
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 std::vector<int> values; for (int i = 0; i < 100; ++i) values.push_back(i); VectorView<int> pairs; VectorView<int> mul3; VectorView<int> mul5; VectorView<int> mul7; VectorView<int> others; CreateLists<int>(values.begin(), values.end() , pairs, [](int v) { return v % 2 == 0; } , mul3, [](int v) { return v % 3 == 0; } , mul5, [](int v) { return v % 5 == 0; } , mul7, [](int v) { return v % 7 == 0; } , others );
Les listes sont créées dans l'ordre où elles apparaissent, une fois qu'un nombre a été ajouté comme multiple de 2, il ne pourra pas aussi se retrouver dans la liste de multiple de 3, 5 ou 7.
Les conditions sont plus simples puisqu'on est assuré que les conditions précédentes ont déjà été épuisées.
Pour y parvenir, il s'agira de méta-programmation relativement simple.
On commence par traiter le cas final, de créer une vue depuis toutes les données restantes :
Puis le cas où l'on a un prédicat pour extraire les données, avec l'utilisation de stable_partition :
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 template<class T> typename std::vector<T>::iterator CreateLists(typename std::vector<T>::iterator first, typename std::vector<T>::iterator end, VectorView<T>& list) { list = VectorView<T>(first, end); return end; }
Enfin, la fonction avec variadic templates qui permet de démarrer le tout :
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 template<class T> typename std::vector<T>::iterator CreateLists(typename std::vector<T>::iterator first, typename std::vector<T>::iterator end, VectorView<T>& list, const std::function<bool(T)>& extracter) { auto firstNonExtracted = std::stable_partition(first, end, extracter); list = VectorView<T>(first, firstNonExtracted); return firstNonExtracted; }
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 template<class T, class... Args> void CreateLists(typename std::vector<T>::iterator first, typename std::vector<T>::iterator end, VectorView<T>& list, const std::function<bool(T)>& extracter, Args&&... args) { auto newFirst = CreateLists<T>(first, end, list, extracter); CreateLists<T>(newFirst, end, std::forward<Args>(args)...); }
Voici le code complet final, avec un test pour extraire des nombres de 0 à 100 :
Ce qui donne ce résultat dans la console :
Code c++ : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 #include <algorithm> #include <functional> #include <iostream> #include <vector> template <class T> class VectorView { public: VectorView() { // Default values to prevent iterating over random memory m_begin = m_end; } VectorView(typename std::vector<T>::const_iterator first, typename std::vector<T>::const_iterator end) : m_begin(first) , m_end(end) {} auto begin() const { return m_begin; } auto end() const { return m_end; } private: typename std::vector<T>::const_iterator m_begin, m_end; }; template<class T> typename std::vector<T>::iterator CreateLists(typename std::vector<T>::iterator first, typename std::vector<T>::iterator end, VectorView<T>& list) { list = VectorView<T>(first, end); return end; } template<class T> typename std::vector<T>::iterator CreateLists(typename std::vector<T>::iterator first, typename std::vector<T>::iterator end, VectorView<T>& list, const std::function<bool(T)>& extracter) { auto firstNonExtracted = std::stable_partition(first, end, extracter); list = VectorView<T>(first, firstNonExtracted); return firstNonExtracted; } template<class T, class... Args> void CreateLists(typename std::vector<T>::iterator first, typename std::vector<T>::iterator end, VectorView<T>& list, const std::function<bool(T)>& extracter, Args&&... args) { auto newFirst = CreateLists<T>(first, end, list, extracter); CreateLists<T>(newFirst, end, std::forward<Args>(args)...); } template<class T> void PrintValues(const char* name, VectorView<T> values) { std::cout << name << " : "; for (T value : values) std::cout << value << ", "; std::cout << std::endl; } #define PRINT_VALUES_WITH_NAME(VALUES) PrintValues(#VALUES, VALUES) int main() { std::vector<int> values; for (int i = 0; i < 100; ++i) values.push_back(i); VectorView<int> pairs; VectorView<int> mul3; VectorView<int> mul5; VectorView<int> mul7; VectorView<int> others; CreateLists<int>(values.begin(), values.end() , pairs, [](int v) { return v % 2 == 0; } , mul3, [](int v) { return v % 3 == 0; } , mul5, [](int v) { return v % 5 == 0; } , mul7, [](int v) { return v % 7 == 0; } , others ); PRINT_VALUES_WITH_NAME(pairs); PRINT_VALUES_WITH_NAME(mul3); PRINT_VALUES_WITH_NAME(mul5); PRINT_VALUES_WITH_NAME(mul7); PRINT_VALUES_WITH_NAME(others); return 0; }
Conclusions
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 pairs : 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, mul3 : 3, 9, 15, 21, 27, 33, 39, 45, 51, 57, 63, 69, 75, 81, 87, 93, 99, mul5 : 5, 25, 35, 55, 65, 85, 95, mul7 : 7, 49, 77, 91, others : 1, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
- Aucune allocation n'a été nécessaire pour trier et extraire les données dans chaque VectorView.
- Les données dans un VectorView sont contiguës, ce qui permet d'itérer de façon effective en limitant les cache-miss.
- L'élément le plus avancé utilisé est std::function, apparu en C++11. Ce code ne nécessite donc que cette version et peut probablement être utilisé dans votre codebase.
- Ce concept/code peut être élargi à toute collection dont les éléments peuvent être triés (array, dequeue, list, queue, stack).
Pour aller plus loin sur ce sujet.