Pourquoi toutes les fonctions <algorithm> ne prennent-elles que des plages, pas des conteneurs?

49

Il y a beaucoup de fonctions utiles dans <algorithm>, mais toutes fonctionnent sur des "séquences" - des paires d'itérateurs. Par exemple, si j’ai un conteneur et que j’aime courir std::accumulatedessus, j’ai besoin d’écrire:

std::vector<int> myContainer = ...;
int sum = std::accumulate(myContainer.begin(), myContainer.end(), 0);

Quand tout ce que je compte faire, c'est:

int sum = std::accumulate(myContainer, 0);

Ce qui est un peu plus lisible et plus clair, à mes yeux.

Maintenant, je peux voir que dans certains cas, vous voudrez peut-être n’utiliser que des parties d’un conteneur. Il est donc certainement utile d’avoir la possibilité de passer des plages. Mais au moins d'après mon expérience, c'est un cas spécial rare. Je veux généralement opérer sur des conteneurs entiers.

Il est facile d'écrire une fonction d'enveloppe qui prend un récipient et des appels begin()et end()à ce sujet , mais ces fonctions pratiques ne sont pas inclus dans la bibliothèque standard.

J'aimerais connaître le raisonnement derrière ce choix de design STL.

guitare mortelle
la source
7
La STL fournit-elle généralement des wrappers pratiques, ou suit-elle l’ancienne politique C-++ «voici-les-outils-maintenant-allez-tirez vous-même-dans-le-pied»?
Kilian Foth
2
Pour l'enregistrement: plutôt que d'écrire votre propre wrapper, vous devez utiliser les wrappers d'algorithmes dans Boost.Range; dans ce cas,boost::accumulate
ecatmur

Réponses:

40

... il est certainement utile d'avoir la possibilité de passer des plages. Mais au moins d'après mon expérience, c'est un cas spécial rare. Je veux généralement opérer sur des conteneurs entiers

Selon votre expérience , il s’agit peut-être d’un cas rare , mais en réalité, le conteneur dans son ensemble constitue le cas particulier et la plage arbitraire le cas général.

Vous avez déjà remarqué que vous pouvez implémenter l' intégralité du conteneur en utilisant l'interface actuelle, mais vous ne pouvez pas faire l'inverse.

Ainsi, le bibliothécaire-rédacteur avait le choix entre implémenter deux interfaces à l’avance ou n’en implémenter qu’une seule qui couvre toujours tous les cas.


Il est facile d'écrire une fonction wrapper qui prend un conteneur et appelle les commandes begin () et end (), mais ces fonctions ne sont pas incluses dans la bibliothèque standard.

C'est vrai, d'autant plus que les fonctions libres std::beginet std::endsont maintenant incluses.

Donc, disons que la bibliothèque fournit la surcharge de commodité:

template <typename Container>
void sort(Container &c) {
  sort(begin(c), end(c));
}

maintenant, il doit également fournir la surcharge équivalente en prenant un foncteur de comparaison, et nous devons fournir les équivalents pour tout autre algorithme.

Mais nous avons au moins couvert tous les cas où nous souhaitons opérer avec un conteneur complet, non? Pas tout à fait. Considérer

std::for_each(c.rbegin(), c.rend(), foo);

Si nous voulons gérer les opérations en arrière sur les conteneurs, nous avons besoin d' une autre méthode (ou d'une paire de méthodes) par algorithme existant.


L’approche basée sur la plage est donc plus générale dans le sens simple qui suit:

  • il peut tout faire de la version contenant entier
  • l'approche globale du conteneur double ou triple le nombre de surcharges requises, tout en restant moins puissante
  • les algorithmes basés sur les plages sont également composables (vous pouvez empiler ou chaîner des adaptateurs d'itérateur, bien que cela se fasse plus couramment dans les langages fonctionnels et en Python)

Bien sûr, il existe une autre raison valable, à savoir que la standardisation du STL nécessitait déjà beaucoup de travail et que le gonfler avec des enveloppes de commodité avant qu'il ne soit largement utilisé ne serait pas une grande utilisation du temps limité des comités. Si vous êtes intéressé, vous trouverez le rapport technique de Stepanov & Lee ici

Comme mentionné dans les commentaires, Boost.Range fournit une approche plus récente sans nécessiter de modification de la norme.

Inutile
la source
9
Je ne pense pas que quiconque, OP compris, suggère d’ajouter des surcharges à chaque cas particulier. Même si "conteneur entier" était moins commun que "une plage arbitraire", il est certainement beaucoup plus commun que "conteneur entier, inversé". Limitez f(c.begin(), c.end(), ...)-le à la surcharge la plus couramment utilisée (quelle que soit votre détermination) pour éviter de doubler le nombre de surcharges. De plus, les adaptateurs d'itérateur sont complètement orthogonaux (comme vous le constatez, ils fonctionnent bien en Python, dont les itérateurs fonctionnent très différemment et qui n'ont pas le pouvoir dont vous parlez).
3
Je suis d’accord pour dire que l’ ensemble du conteneur, le cas d’ expédition est très courant, mais je tiens à préciser qu’il s’agit d’un sous-ensemble beaucoup plus petit d’utilisations possibles que la question suggérée. En particulier parce que le choix ne consiste pas entre un conteneur entier et un conteneur partiel, mais entre un conteneur entier et un conteneur partiel, éventuellement inversé ou autrement adapté. Et je pense qu'il est juste de suggérer que la complexité perçue de l'utilisation d'adaptateurs est plus grande, si vous devez également modifier la surcharge de votre algorithme.
Inutile
23
Notez que la version conteneur serait couvrir tous les cas , si la STL a offert un objet de plage; par exemple std::sort(std::range(start, stop)).
3
Au contraire: les algorithmes fonctionnels composables (tels que map et filter) prennent un seul objet qui représente une collection et renvoient un seul objet, ils n'utilisent certainement rien qui ressemble à une paire d'itérateurs.
svick
3
une macro pourrait faire ceci: #define MAKE_RANGE(container) (container).begin(), (container).end()</ jk>
freak freak
21

Il s'avère qu'il y a un article de Herb Sutter sur ce sujet même. Fondamentalement, le problème est l’ambiguïté de surcharge. Compte tenu de ce qui suit:

template<typename Iter>
void sort( Iter, Iter ); // 1

template<typename Iter, typename Pred>
void sort( Iter, Iter, Pred ); // 2

Et en ajoutant ce qui suit:

template<typename Container>
void sort( Container& ); // 3

template<typename Container, typename Pred>
void sort( Container&, Pred ); // 4

Cela rendra difficile à distinguer 4et 1correctement.

Les concepts, tels que proposés mais finalement pas inclus dans C ++ 0x, auraient résolu ce problème, et il est également possible de les contourner à l'aide de enable_if. Pour certains algorithmes, cela ne pose aucun problème. Mais ils ont décidé de ne pas le faire.

Maintenant, après avoir lu tous les commentaires et réponses ici, je pense que les rangeobjets seraient la meilleure solution. Je pense que je vais regarder Boost.Range.

guitare mortelle
la source
1
Eh bien, utiliser simplement a typename Itersemble être trop typé pour un langage strict. Je préférerais par exemple template<typename Container> void sort(typename Container::iterator, typename Container::iterator); // 1et template<template<class> Container, typename T> void sort( Container<T>&, std::function<bool(const T&)> ); // 4etc. (ce qui résoudrait peut-être le problème de l'ambiguïté)
Vlad
@ Vlad: Malheureusement, cela ne fonctionnera pas pour les vieux tableaux, car il n'y en a pas T[]::iterator. En outre, un itérateur approprié n'est pas obligé d'être un type imbriqué d'une collection, il suffit de le définir std::iterator_traits.
firegurafiku
@firegurafiku: Eh bien, les tableaux sont faciles à utiliser avec des astuces de base sur le TMP.
Vlad
11

Fondamentalement, une décision héritée. Le concept d'itérateur est calqué sur les pointeurs, mais les conteneurs ne sont pas calqués sur des tableaux. De plus, comme les tableaux sont difficiles à transmettre (en général, il leur faut un paramètre de modèle non typé pour la longueur), très souvent, une fonction ne dispose que de pointeurs.

Mais oui, avec le recul, la décision est fausse. Nous aurions mieux fait d'utiliser un objet range pouvant être construit à partir de begin/endou begin/length; nous avons maintenant plusieurs _nalgorithmes suffixés.

MSalters
la source
5

Leur ajout ne vous apporterait aucun pouvoir (vous pouvez déjà traiter tout le conteneur en appelant vous .begin()- .end()même), et cela ajouterait une dernière chose à la bibliothèque qui doit être correctement spécifiée, ajoutée aux bibliothèques par les fournisseurs, testée, maintenue. etc.

En bref, ce n'est probablement pas là car il ne vaut pas la peine de conserver un ensemble de modèles supplémentaires simplement pour éviter aux utilisateurs de conteneur entier de saisir un paramètre d'appel de fonction supplémentaire.

Michael Kohne
la source
9
Cela ne me donnerait pas le pouvoir, c'est vrai - mais au final, ce n'est pas le cas std::getline, et pourtant, c'est dans la bibliothèque. On pourrait même aller jusqu'à dire que les structures de contrôle étendues ne me permettent pas de gagner du pouvoir, car je peux tout faire en utilisant seulement ifet goto. Ouais, comparaison injuste, je sais;) Je pense que je peux comprendre le fardeau de la spécification / mise en œuvre / maintenance d'une manière ou d'une autre, mais nous ne parlons ici que d'une petite enveloppe, alors ..
guitare mortelle
Une petite enveloppe ne coûte rien au code et cela n’a aucun sens d’être dans la bibliothèque.
ebasconp
-1

A ce jour, http://en.wikipedia.org/wiki/C++11#Range-based_for_loop est une bonne alternative à std::for_each. Observez, pas d'itérateurs explicites:

int a[5] = {1, 2, 3, 4, 5};
for (auto &i: a) { i *= 2; }

(Inspiré par https://stackoverflow.com/a/694534/2097284 .)

Camille Goudeseune
la source
1
Il ne résout que pour cette seule partie de <algorithm>, pas tous les algues qui en ont besoin beginet les enditérateurs - mais le bénéfice ne peut pas être surestimé! Quand j'ai essayé pour la première fois le C ++ 03 en 2009, je me suis écarté des itérateurs à cause du passe-partout de la boucle, et heureusement ou non, mes projets de l'époque l'ont permis. En redémarrant sur C ++ 11 en 2014, ce fut une mise à niveau incroyable, le langage que C ++ aurait toujours dû être, et je ne peux plus vivre sans auto &it: them:)
underscore_d