Perché tutte le funzioni <algoritmo> accettano solo intervalli, non contenitori?


49

Ci sono molte funzioni utili in <algorithm>, ma tutte operano su "sequenze" - coppie di iteratori. Ad esempio, se ho un contenitore e mi piace eseguirlo std::accumulate, devo scrivere:

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

Quando tutto ciò che intendo fare è:

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

Che è un po 'più leggibile e più chiaro, ai miei occhi.

Ora posso vedere che potrebbero esserci casi in cui vorresti operare solo su parti di un contenitore, quindi è sicuramente utile avere l' opzione di passare intervalli. Ma almeno nella mia esperienza, è un caso speciale raro. Di solito vorrei operare su interi contenitori.

È facile scrivere una funzione wrapper che accetta un contenitore e chiama begin()e end()su di esso, ma tali funzioni di convenienza non sono incluse nella libreria standard.

Mi piacerebbe conoscere il ragionamento alla base di questa scelta di design STL.


7
L'STL in genere fornisce wrapper convenienti o segue la vecchia politica C ++ ecco-gli-strumenti-ora-vai-sparare-da-te?
Kilian Foth,

2
Per la cronaca: piuttosto che scrivere il proprio wrapper dovresti usare gli algoritmi wrapper in Boost.Range; in questo caso,boost::accumulate
ecatmur

Risposte:


40

... è sicuramente utile avere la possibilità di passare intervalli. Ma almeno nella mia esperienza, è un caso speciale raro. Di solito vorrei operare su interi contenitori

Potrebbe trattarsi di un raro caso speciale nella tua esperienza , ma in realtà l' intero contenitore è il caso speciale e l' intervallo arbitrario è il caso generale.

Hai già notato che puoi implementare l' intera custodia del contenitore utilizzando l'interfaccia corrente, ma non puoi fare il contrario.

Quindi, lo scrittore di biblioteche poteva scegliere tra l'implementazione di due interfacce in anticipo, o solo l'implementazione di una che copre ancora tutti i casi.


È facile scrivere una funzione wrapper che accetta un contenitore e chiama da inizio () e fine (), ma tali funzioni di convenienza non sono incluse nella libreria standard

Vero, soprattutto perché le funzioni gratuite std::begine std::endora sono incluse.

Quindi, supponiamo che la libreria fornisca il sovraccarico di convenienza:

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

ora deve anche fornire il sovraccarico equivalente prendendo un funzione di confronto e dobbiamo fornire gli equivalenti per ogni altro algoritmo.

Ma almeno abbiamo coperto tutti i casi in cui vogliamo operare su un contenitore pieno, giusto? Bene, non proprio. Tener conto di

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

Se vogliamo gestire il funzionamento all'indietro sui container, abbiamo bisogno di un altro metodo (o coppia di metodi) per algoritmo esistente.


Quindi, l'approccio basato sul range è più generale nel senso semplice che:

  • può fare tutto ciò che può fare la versione dell'intero contenitore
  • l'approccio dell'intero contenitore raddoppia o triplica il numero di sovraccarichi richiesti, pur essendo meno potente
  • gli algoritmi basati su intervallo sono anche componibili (è possibile impilare o concatenare adattatori iteratori, sebbene ciò sia più comunemente fatto in linguaggi funzionali e Python)

C'è un'altra ragione valida, ovviamente, che è che era già molto lavoro per standardizzare la STL, e gonfiarla con involucri di comodo prima che fosse ampiamente usata non sarebbe un grande uso del tempo limitato del comitato. Se sei interessato, puoi trovare il rapporto tecnico di Stepanov & Lee qui

Come menzionato nei commenti, Boost.Range offre un approccio più recente senza richiedere modifiche allo standard.


9
Non credo che nessuno, OP compreso, stia suggerendo di aggiungere sovraccarichi per ogni singolo caso speciale. Anche se "intero contenitore" era meno comune di "un intervallo arbitrario", è sicuramente molto più comune di "intero contenitore, invertito". Limita a f(c.begin(), c.end(), ...), e forse solo il sovraccarico più comunemente usato (comunque lo determini) per evitare di raddoppiare il numero di sovraccarichi. Inoltre, gli adattatori di iteratori sono completamente ortogonali (come noti, funzionano bene in Python, i cui iteratori funzionano in modo molto diverso e non hanno la maggior parte del potere di cui parli).

3
Concordo sull'intero contenitore, il caso forward è molto comune, ma volevo sottolineare che si tratta di un sottoinsieme molto più piccolo di possibili usi rispetto alla domanda suggerita. In particolare perché la scelta non è tra l'intero contenitore e il contenitore parziale, ma tra l'intero contenitore e il contenitore parziale, eventualmente invertito o altrimenti adattato. E penso che sia giusto suggerire che la complessità percepita dell'uso degli adattatori è maggiore, anche se è necessario modificare il sovraccarico dell'algoritmo.
Inutile

23
Nota la versione del contenitore sarebbe coprire tutti i casi se lo STL ha offerto un oggetto Range; es std::sort(std::range(start, stop)).

3
Al contrario: gli algoritmi funzionali componibili (come mappa e filtro) prendono un singolo oggetto che rappresenta una raccolta e restituiscono un singolo oggetto, di certo non usano nulla che assomigli a una coppia di iteratori.
svick

3
una macro potrebbe fare questo: #define MAKE_RANGE(container) (container).begin(), (container).end()</jk>
maniaco del cricchetto

21

Si scopre che c'è un articolo di Herb Sutter proprio su questo argomento. Fondamentalmente, il problema è l'ambiguità del sovraccarico. Dato quanto segue:

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

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

E aggiungendo quanto segue:

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

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

Renderà difficile distinguere 4e 1correttamente.

I concetti, come proposti ma alla fine non inclusi in C ++ 0x, avrebbero risolto questo problema ed è anche possibile aggirarlo usando enable_if. Per alcuni algoritmi, non è affatto un problema. Ma hanno deciso di non farlo.

Ora dopo aver letto tutti i commenti e le risposte qui, penso che gli rangeoggetti sarebbero la soluzione migliore. Penso che darò un'occhiata Boost.Range.


1
Bene, usare solo a typename Itersembra essere troppo dattiloscritto per un linguaggio rigoroso. Io preferirei ad esempio, template<typename Container> void sort(typename Container::iterator, typename Container::iterator); // 1e template<template<class> Container, typename T> void sort( Container<T>&, std::function<bool(const T&)> ); // 4così via (che forse risolvere il problema dell'ambiguità)
Vlad

@Vlad: Sfortunatamente, questo non funzionerà con i vecchi array semplici, poiché non è T[]::iteratordisponibile. Inoltre, l'iteratore corretto non è obbligato a essere un tipo nidificato di qualsiasi raccolta, è sufficiente definirlo std::iterator_traits.
firegurafiku,

@firegurafiku: Bene, gli array sono facili da usare in casi speciali con alcuni trucchi TMP di base.
Vlad,

11

Fondamentalmente una decisione legacy. Il concetto di iteratore è modellato su puntatori, ma i contenitori non sono modellati su matrici. Inoltre, poiché le matrici sono difficili da passare (in genere è necessario un parametro di modello non di tipo per la lunghezza), abbastanza spesso una funzione ha solo puntatori disponibili.

Ma sì, col senno di poi la decisione è sbagliata. Saremmo stati meglio con un oggetto range costruibile da begin/endo begin/length; ora abbiamo invece più _nalgoritmi con suffisso.


5

Aggiungerli non otterresti alcun potere (puoi già fare l'intero contenitore chiamando .begin()e .end()te stesso) e aggiungerebbe un'altra cosa alla libreria che deve essere specificata correttamente, aggiunta alle librerie dai fornitori, testata, mantenuta, ecc, ecc.

In breve, probabilmente non è lì perché non vale la pena mantenere un set di template extra solo per salvare gli utenti dell'intero container dalla digitazione di un parametro di chiamata di funzione extra.


9
Non mi guadagnerebbe potere, è vero, ma alla fine, né lo fa std::getline, e tuttavia, è in biblioteca. Si potrebbe arrivare al punto di dire che le strutture di controllo estese non mi guadagnano il potere, dato che potrei fare tutto usando solo ife goto. Sì, confronto ingiusto, lo so;) Penso di poter capire in qualche modo l'onere della specifica / implementazione / manutenzione, ma è solo un piccolo involucro di cui stiamo parlando qui, quindi ..
lethal-guitar

Un piccolo wrapper non costa nulla da codificare e forse non ha alcun senso essere nella libreria.
ebasconp,

-1

Ormai, http://en.wikipedia.org/wiki/C++11#Range-based_for_loop è una bella alternativa a std::for_each. Osservare, nessun iteratore esplicito:

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

(Ispirato da https://stackoverflow.com/a/694534/2097284 .)


1
Risolve solo per quella singola parte di <algorithm>, non tutti gli algoritmi reali di cui hanno bisogno begine enditeratori - ma il vantaggio non può essere sopravvalutato! Quando ho provato per la prima volta C ++ 03 nel 2009, mi sono allontanato dagli iteratori a causa della placca di looping e, fortunatamente o no, i miei progetti all'epoca lo hanno permesso. Riavviando su C ++ 11 nel 2014, è stato un aggiornamento incredibile, il linguaggio C ++ avrebbe sempre dovuto essere, e ora non posso vivere senza auto &it: them:)
underscore_d
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.