Preferisci gli algoritmi ai loop scritti a mano?


10

Quale delle seguenti è più leggibile? Il ciclo scritto a mano:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

O l'invocazione dell'algoritmo:

#include <algorithm>
#include <functional>

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Mi chiedo se ne std::for_eachvalga davvero la pena, dato un esempio così semplice richiede già così tanto codice.

Cosa ne pensi al riguardo?


2
Per C ++, la risposta è abbastanza ovvia. Ma in altre lingue, entrambi sono quasi uguali (ad esempio Python:, map(bar.process, vec)sebbene la mappa per gli effetti collaterali sia scoraggiata e la comprensione dell'elenco / le espressioni del generatore sono raccomandate sulla mappa).

5
E poi c'è anche BOOST_FOREACH...
Benjamin Bannier,

Quindi, l'elemento nell'STL efficace non ti ha convinto? stackoverflow.com/questions/135129/…
Lavoro

Risposte:


18

C'è una ragione per cui sono stati introdotti i lambda ed è perché anche il Comitato Standard riconosce che la seconda forma fa schifo. Utilizzare il primo modulo, fino a quando non si ottiene il supporto C ++ 0x e lambda.


2
Lo stesso ragionamento può essere usato per giustificare la seconda forma: il Comitato Standard ha riconosciuto che era così superiore che introdussero lambda per renderlo ancora migliore. :-p (+1 comunque)
Konrad Rudolph,

Non credo che il secondo sia così male. Ma può essere migliore. Ad esempio, hai deliberatamente reso più difficile l'uso dell'oggetto Bar non aggiungendo operator (). Usando questo semplifica notevolmente il punto di chiamata nel ciclo e rende il codice ordinato.
Martin York,

Nota: il supporto lambda è stato aggiunto a causa di due lamentele importanti. 1) La piastra di caldaia standard necessaria per trasmettere i parametri ai funzioni. 2) Non avere il codice al punto di chiamata. Il tuo codice non soddisfa nessuno di questi come stai solo chiamando un metodo. Quindi 1) nessun codice aggiuntivo per il passaggio dei parametri 2) Il codice non è già al punto di chiamata, quindi lambda non migliorerà la manutenibilità del codice. Quindi sì, le lambda sono un'ottima aggiunta. Questo non è un buon argomento per loro.
Martin York,

9

Usa sempre la variante che descrive meglio ciò che intendi fare. Questo è

Per ogni elemento xin vec, do bar.process(x).

Ora esaminiamo gli esempi:

std::for_each(vec.begin(), vec.end(),
              std::bind1st(std::mem_fun_ref(&Bar::process), bar));

Abbiamo anche un for_eachlì - yippeh . Abbiamo la [begin; end)gamma su cui vogliamo operare.

In linea di principio, l'algoritmo era molto più esplicito e quindi preferibile rispetto a qualsiasi implementazione scritta a mano. Ma poi ... Raccoglitori? Memfun? Fondamentalmente C ++ interna su come ottenere una funzione membro? Per il mio compito, non mi importa di loro! Né voglio soffrire di questa sintassi dettagliata e inquietante.

Ora l'altra possibilità:

for (std::vector<Foo>::const_iterator it = vec.begin(); it != vec.end(); ++it)
{
    bar.process(*it);
}

Certo, questo è un modello comune da riconoscere, ma ... creazione di iteratori, loop, incremento, dereferenziazione. Anche queste sono tutte cose che non mi interessano per svolgere il mio compito.

Certo, sembra meglio della prima soluzione (almeno, il corpo del loop è flessibile ed abbastanza esplicito), ma non è poi così eccezionale. Useremo questo se non avessimo possibilità migliori, ma forse abbiamo ...

Un modo migliore?

Ora torniamo a for_each. Non sarebbe bello dire letteralmente for_each ed essere flessibile anche nell'operazione che deve essere fatta? Fortunatamente, dal C ++ 0x lambdas, lo siamo

for_each(v.begin(), v.end(), [&](const Foo& x) { bar.process(x); })

Ora che abbiamo trovato una soluzione astratta e generica a molte situazioni correlate, vale la pena notare che in questo caso particolare, c'è un preferito in assoluto # 1 :

foreach(const Foo& x, vec) bar.process(x);

Non può essere molto più chiaro di così. Per fortuna, C ++ 0x get è una sintassi simile incorporata !


2
Detto essere "sintassi simile" for (const Foo& x : vec) bar.process(x);, o usando const auto&se vuoi.
Jon Purdy,

const auto&è possibile? Non lo sapevo - ottime informazioni!
Dario,

8

Perché è così illeggibile?

for (unsigned int i=0;i<vec.size();i++) {
{
    bar.process(vec[i]);
}

4
Ci dispiace, ma per qualche motivo dice "censurato" dove credo che dovrebbe essere il tuo codice.
Mateen Ulhaq,

6
Il tuo codice dà un avviso al compilatore, perché confrontare un intcon un size_tè pericoloso. Inoltre, l'indicizzazione non funziona con tutti i contenitori, ad esempio std::set.
fredoverflow,

2
Questo codice funziona solo per i contenitori con un operatore di indicizzazione, di cui ce ne sono pochi. Lega l'algoritmo in modo inconsueto: cosa succederebbe se una mappa <> si adattasse meglio? L'originale per loop e for_each non sarebbero interessati.
JBR Wilkinson,

2
E quante volte progetti il ​​codice per un vettore e poi decidi di scambiarlo con una mappa? Come se progettare per quello fosse la priorità delle lingue.
Martin Beckett,

2
Scorrere le raccolte per indice è sconsigliato perché alcune raccolte hanno scarse prestazioni sull'indicizzazione, mentre tutte le raccolte si comportano bene quando si utilizza un iteratore.
slaphappy

3

in generale, la prima forma è leggibile praticamente da chiunque sappia cos'è un for-loop, indipendentemente dal loro background.

anche in generale, il secondo non è affatto leggibile: abbastanza facile capire cosa sta facendo for_each, ma se non l'hai mai visto std::bind1st(std::mem_fun_refposso immaginare che sia difficile da capire.


2

In realtà, anche con C ++ 0x, non sono sicuro for_eachche otterrà molto amore.

for(Foo& foo: vec) { bar.process(foo); }

È molto più leggibile.

L'unica cosa che non mi piace degli algoritmi (in C ++) è il loro ragionamento su iteratori, rende dichiarazioni molto dettagliate.


2

Se avessi scritto la barra come funzione, sarebbe molto più semplice:

// custom functor
class Bar
{    public: void operator()(Foo& value) { /* STUFF */ }
};


std::for_each(vec.begin(), vec.end(), Bar());

Qui il codice è abbastanza leggibile.


2
È abbastanza leggibile a parte il fatto che hai appena scritto un funzione.
Winston Ewert,

Vedi qui per perché penso che questo codice sia peggio.
sabato

1

Preferisco quest'ultima perché è pulita e ordinata. In realtà fa parte di molti altri linguaggi ma in C ++ fa parte della libreria. Non importa davvero.


0

Ora questo problema in realtà non è specifico del C ++, direi.

Il fatto è che passare qualche funzione in un'altra funzione non ha lo scopo di sostituire i loop .
Dovrebbe semplificare alcuni schemi, che diventano molto naturali con la programmazione funzionale, come visitatore e osservatore , solo per citarne due, che mi vengono subito in mente.
Nei linguaggi privi di funzioni del primo ordine (Java è probabilmente l'esempio migliore), tali approcci richiedono sempre l'implementazione di una determinata interfaccia, che è piuttosto verbo e ridondante.

Un uso comune che vedo molto in altre lingue sarebbe:

someCollection.forEach(someUnaryFunctor);

Il vantaggio è che non è necessario sapere come someCollectionviene effettivamente implementato o cosa someUnaryFunctorfa. Tutto quello che devi sapere è che il suo forEachmetodo eseguirà l'iterazione di tutti gli elementi della raccolta, passando a una determinata funzione.

Personalmente, se sei nella posizione, per avere tutte le informazioni sulla struttura dei dati su cui vuoi iterare e tutte le informazioni su ciò che vuoi fare in ogni fase dell'iterazione, un approccio funzionale sta complicando le cose, soprattutto in una lingua, dove questo è apparentemente abbastanza noioso.

Inoltre, dovresti tenere a mente che l'approccio funzionale è più lento, perché hai molte chiamate, che hanno un certo costo, che non hai in un ciclo for.


1
"Inoltre, dovresti tenere a mente che l'approccio funzionale è più lento, perché hai molte chiamate, che hanno un certo costo, che non hai in un ciclo for." ? Questa affermazione non è supportata. L'approccio funzionale non è necessariamente più lento, soprattutto perché potrebbero esserci delle ottimizzazioni sotto il cofano. E anche se capisci completamente il tuo loop, probabilmente sembrerà più pulito nella sua forma più astratta.
Andres F.

@Andres F: Quindi sembra più pulito? std::for_each(vec.begin(), vec.end(), std::bind1st(std::mem_fun_ref(&Bar::process), bar));Ed è più lento in fase di esecuzione o di compilazione (se sei fortunato). Non vedo perché la sostituzione delle strutture di controllo del flusso con chiamate di funzioni renda il codice migliore. Informazioni su qualsiasi alternativa qui presentata è più leggibile.
back2dos,

0

Penso che il problema qui sia che for_eachnon è proprio un algoritmo, è solo un modo diverso (e di solito inferiore) di scrivere un ciclo scritto a mano. da questo punto di vista dovrebbe essere l'algoritmo standard meno utilizzato, e sì probabilmente potresti anche usare un ciclo for. tuttavia il consiglio nel titolo rimane valido in quanto vi sono diversi algoritmi standard su misura per usi più specifici.

Dove c'è un algoritmo che fa più strettamente quello che vuoi, allora sì, dovresti preferire gli algoritmi ai loop scritti a mano. esempi ovvi qui sono l'ordinamento con sorto stable_sorto la ricerca con lower_bound, upper_boundo equal_range, ma la maggior parte di essi presenta una situazione in cui è preferibile utilizzare un ciclo codificato manualmente.


0

Per questo piccolo frammento di codice, entrambi sono ugualmente leggibili per me. In generale, trovo che i cicli manuali siano soggetti a errori e preferisco usare algoritmi, ma dipende davvero da una situazione concreta.

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.