Come posso evitare i cicli "for" con una condizione "if" al loro interno con C ++?


111

Con quasi tutto il codice che scrivo, spesso ho a che fare con problemi di riduzione dei set su raccolte che alla fine si ritrovano con ingenue condizioni "se" al loro interno. Ecco un semplice esempio:

for(int i=0; i<myCollection.size(); i++)
{
     if (myCollection[i] == SOMETHING)
     {
           DoStuff();
     }
}

Con i linguaggi funzionali, posso risolvere il problema riducendo la raccolta a un'altra raccolta (facilmente) e quindi eseguire tutte le operazioni sul mio set ridotto. In pseudocodice:

newCollection <- myCollection where <x=true
map DoStuff newCollection

E in altre varianti C, come C #, potrei ridurre con una clausola where come

foreach (var x in myCollection.Where(c=> c == SOMETHING)) 
{
   DoStuff();
}

O meglio (almeno ai miei occhi)

myCollection.Where(c=>c == Something).ToList().ForEach(d=> DoStuff(d));

Certo, sto facendo un sacco di mix di paradigmi e stili soggettivi / basati su opinioni, ma non posso fare a meno di sentire che mi manca qualcosa di veramente fondamentale che potrebbe permettermi di usare questa tecnica preferita con C ++. Qualcuno potrebbe illuminarmi?


7
Fuori dalla funzionalità della libreria standard C ++, puoi provare std::copy_if, ma le selezioni non sono pigre
milleniumbug

14
Potresti essere interessato a range-v3 . Dovrebbe anche arrivare a C ++ come TS e, si spera, standardizzato in una versione futura.
NathanOliver

12
Sento il bisogno di sottolineare che l' ifinterno a forche citi non solo è praticamente equivalente dal punto di vista funzionale agli altri esempi, ma probabilmente sarebbe anche più veloce in molti casi. Anche per qualcuno a cui piace lo stile funzionale, ciò che stai promuovendo sembra andare contro il concetto di purezza tanto amato dalla programmazione funzionale poiché DoStuffha chiaramente effetti collaterali.
Pharap

60
Non ho mai veramente capito perché le persone colleghino combinando tutta la logica su una singola riga lo fa sembrare in qualche modo migliore o più leggibile. Il tuo frammento C ++ in cima è di gran lunga il più leggibile tra tutte le tue possibilità. E poiché l'efficienza non verrà modificata, non riesco a capire perché preferiresti non scriverlo, a meno che tu non sia pagato per il numero di righe di codice che elimini.
Cody Grey

10
@CodyGray d'accordo: e 'solo zucchero sintattico. E il titolo della domanda è fuorviante, perché è molto diverso evitando di ramificarsi e nascondendolo sotto astrazione.
edmz

Risposte:


99

IMHO è più semplice e più leggibile usare un ciclo for con un if al suo interno. Tuttavia, se questo è fastidioso per te, potresti usarne uno for_each_ifcome quello qui sotto:

template<typename Iter, typename Pred, typename Op> 
void for_each_if(Iter first, Iter last, Pred p, Op op) {
  while(first != last) {
    if (p(*first)) op(*first);
    ++first;
  }
}

Caso d'uso:

std::vector<int> v {10, 2, 10, 3};
for_each_if(v.begin(), v.end(), [](int i){ return i > 5; }, [](int &i){ ++i; });

Dimostrazione dal vivo


10
Questo è eccezionalmente intelligente. Sono anche d'accordo sul fatto che non è semplice e probabilmente userò solo le condizioni if ​​durante la programmazione di C ++ che viene consumato da altri. Ma è esattamente ciò di cui ho bisogno per il mio uso personale! :)
Darkenor

14
@Default Passare coppie di iteratori piuttosto che contenitori è C ++ più flessibile e idiomatico.
Mark B

8
@Slava, in generale gli intervalli non ridurranno il numero di algoritmi. Ad esempio, hai ancora bisogno find_ife findse funzionano su intervalli o coppie di iteratori. (Ci sono alcune eccezioni, come for_eache for_each_n). Il modo per evitare di scrivere nuovi algoritmi per ogni starnuto è utilizzare operazioni diverse con gli algoritmi esistenti, ad esempio invece di for_each_ifincorporare la condizione nel chiamabile passato a for_each, ad esempiofor_each(first, last, [&](auto& x) { if (cond(x)) f(x); });
Jonathan Wakely

9
Devo essere d'accordo con la prima frase: la soluzione standard for-if è molto più leggibile e più facile da usare. Penso che la sintassi lambda e l'uso di un modello definito altrove solo per gestire un semplice ciclo irriterebbero o forse confonderebbero altri sviluppatori. Stai sacrificando località e performance per ... cosa? Riuscire a scrivere qualcosa in una riga?
user1354557

45
Cough @Darkenor, generalmente la programmazione " eccezionalmente intelligente" è da evitare perché infastidisce tutti gli altri, incluso il tuo sé futuro.
Ryan

48

Boost fornisce intervalli che possono essere utilizzati in base a intervalli per. Gli intervalli hanno il vantaggio di non copiare la struttura dei dati sottostante, ma forniscono semplicemente una "vista" (ovvero begin(), end()per l'intervallo e operator++(), operator==()per l'iteratore). Questo potrebbe essere di tuo interesse: http://www.boost.org/libs/range/doc/html/range/reference/adaptors/reference/filtered.html

#include <boost/range/adaptor/filtered.hpp>
#include <iostream>
#include <vector>

struct is_even
{
    bool operator()( int x ) const { return x % 2 == 0; }
};

int main(int argc, const char* argv[])
{
    using namespace boost::adaptors;

    std::vector<int> myCollection{1,2,3,4,5,6,7,8,9};

    for( int i: myCollection | filtered( is_even() ) )
    {
        std::cout << i;
    }
}

1
Posso suggerire di utilizzare invece l'esempio degli OP, cioè is_even=> condition, input=> myCollectionecc.
Predefinito

Questa è una risposta davvero eccellente e sicuramente quello che sto cercando di fare. Mi trattengo dall'accettare a meno che qualcuno non riesca a trovare un modo conforme agli standard per farlo che utilizzi l'esecuzione lenta / differita. Upvoted.
Darkenor

5
@Darkenor: Se Boost è un problema per te (ad esempio, ti è stato vietato di usarlo a causa della politica aziendale e della saggezza del manager), posso trovare una definizione semplificata filtered()per te - detto questo, è meglio usare una libreria supportata rispetto a un codice ad-hoc.
lorro

Totalmente d'accordo con te. L'ho accettato perché il modo conforme agli standard è venuto prima perché la domanda era orientata al C ++ stesso, non alla libreria boost. Ma questo è davvero eccellente. Inoltre - sì, ho purtroppo lavorato in molti posti che hanno vietato il Boost per ragioni assurde ...
Darkenor

@LeeClagett:? .
lorro

44

Invece di creare un nuovo algoritmo, come fa la risposta accettata, puoi usarne uno esistente con una funzione che applica la condizione:

std::for_each(first, last, [](auto&& x){ if (cond(x)) { ... } });

O se vuoi davvero un nuovo algoritmo, almeno riutilizzalo for_eachlì invece di duplicare la logica di iterazione:

template<typename Iter, typename Pred, typename Op> 
  void
  for_each_if(Iter first, Iter last, Pred p, Op op) {
    std::for_each(first, last, [&](auto& x) { if (p(x)) op(x); });
  }

Molto meglio e più chiaro per l'utilizzo della libreria standard.
anonimo

4
Perché std::for-each(first, last, [&](auto& x) {if (p(x)) op(x); });è totalmente più semplice di for (Iter x = first; x != last; x++) if (p(x)) op(x);}?
user253751

2
Il riutilizzo della libreria standard da parte di @immibis ha altri vantaggi, come il controllo della validità dell'iteratore o (in C ++ 17) è molto più facile parallelizzare, semplicemente aggiungendo un altro argomento: std::for_each(std::execution::par, first, last, ...);quanto è facile aggiungere queste cose a un ciclo scritto a mano?
Jonathan Wakely

1
#pragma omp parallel for
Mark K Cowan

2
@mark mi dispiace, qualche stranezza casuale del codice sorgente o della catena di build ha fatto sì che l'estensione del compilatore parallela non standard fastidiosamente fragile non generi un aumento delle prestazioni senza diagnostica.
Yakk - Adam Nevraumont

21

L'idea di evitare

for(...)
    if(...)

come un antipattern è troppo ampio.

È completamente corretto elaborare più elementi che corrispondono a una determinata espressione dall'interno di un ciclo e il codice non può essere molto più chiaro di così. Se l'elaborazione diventa troppo grande per adattarsi allo schermo, questa è una buona ragione per usare una subroutine, ma comunque il condizionale è meglio posizionato all'interno del ciclo, cioè

for(...)
    if(...)
        do_process(...);

è di gran lunga preferibile a

for(...)
    maybe_process(...);

Diventa un antipattern quando un solo elemento corrisponderà, perché allora sarebbe più chiaro cercare prima l'elemento ed eseguire l'elaborazione al di fuori del ciclo.

for(int i = 0; i < size; ++i)
    if(i == 5)

ne è un esempio estremo e ovvio. Più sottile, e quindi più comune, è un modello di fabbrica simile

for(creator &c : creators)
    if(c.name == requested_name)
    {
        unique_ptr<object> obj = c.create_object();
        obj.owner = this;
        return std::move(obj);
    }

Questo è difficile da leggere, perché non è ovvio che il codice del corpo verrà eseguito una sola volta. In questo caso, sarebbe meglio separare la ricerca:

creator &lookup(string const &requested_name)
{
    for(creator &c : creators)
        if(c.name == requested_name)
            return c;
}

creator &c = lookup(requested_name);
unique_ptr obj = c.create_object();

C'è ancora un ifall'interno di a for, ma dal contesto diventa chiaro cosa fa, non c'è bisogno di cambiare questo codice a meno che la ricerca non cambi (es. In a map), ed è subito chiaro che create_object()viene chiamato solo una volta, perché è non all'interno di un ciclo.


Mi piace questo, in quanto panoramica ponderata ed equilibrata, anche se in un certo senso rifiuta di rispondere alla domanda posta. Trovo che lo for( range ){ if( condition ){ action } }stile renda facile leggere le cose un pezzo alla volta e utilizzi solo la conoscenza dei costrutti linguistici di base.
PJTraill

@PJTraill, il modo in cui la domanda è stata formulata mi ha ricordato lo sfogo di Raymond Chen contro il for-if antipattern , che è stato cultato dal carico e in qualche modo è diventato un assoluto. Sono totalmente d'accordo che for(...) if(...) { ... }spesso è la scelta migliore (ecco perché ho qualificato la raccomandazione per dividere l'azione in una subroutine).
Simon Richter

1
Grazie per il collegamento, che mi ha chiarito le cose: il nome " per-se " è fuorviante e dovrebbe essere qualcosa come " per-tutti-se-uno " o " ricerca-evita ". Mi ricorda il modo in cui l' inversione di astrazione è stata descritta da Wikipedia nel 2005 come quando si " crea semplici costrutti sopra complessi (quelli)" - finché non l'ho riscritto! In realtà non mi affretterei nemmeno a correggere la forma di uscita dal processo di ricerca for(…)if(…)…se fosse l'unico posto in cui si è verificata la ricerca.
PJTraill

17

Ecco una funzione rapida relativamente minima filter.

Ci vuole un predicato. Restituisce un oggetto funzione che accetta un iterabile.

Restituisce un iterabile che può essere utilizzato in un for(:)ciclo.

template<class It>
struct range_t {
  It b, e;
  It begin() const { return b; }
  It end() const { return e; }
  bool empty() const { return begin()==end(); }
};
template<class It>
range_t<It> range( It b, It e ) { return {std::move(b), std::move(e)}; }

template<class It, class F>
struct filter_helper:range_t<It> {
  F f;
  void advance() {
    while(true) {
      (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      if (this->empty())
        return;
      if (f(*this->begin()))
        return;
    }
  }
  filter_helper(range_t<It> r, F fin):
    range_t<It>(r), f(std::move(fin))
  {
      while(true)
      {
          if (this->empty()) return;
          if (f(*this->begin())) return;
          (range_t<It>&)*this = range( std::next(this->begin()), this->end() );
      }
  }
};

template<class It, class F>
struct filter_psuedo_iterator {
  using iterator_category=std::input_iterator_tag;
  filter_helper<It, F>* helper = nullptr;
  bool m_is_end = true;
  bool is_end() const {
    return m_is_end || !helper || helper->empty();
  }

  void operator++() {
    helper->advance();
  }
  typename std::iterator_traits<It>::reference
  operator*() const {
    return *(helper->begin());
  }
  It base() const {
      if (!helper) return {};
      if (is_end()) return helper->end();
      return helper->begin();
  }
  friend bool operator==(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    if (lhs.is_end() && rhs.is_end()) return true;
    if (lhs.is_end() || rhs.is_end()) return false;
    return lhs.helper->begin() == rhs.helper->begin();
  }
  friend bool operator!=(filter_psuedo_iterator const& lhs, filter_psuedo_iterator const& rhs) {
    return !(lhs==rhs);
  }
};
template<class It, class F>
struct filter_range:
  private filter_helper<It, F>,
  range_t<filter_psuedo_iterator<It, F>>
{
  using helper=filter_helper<It, F>;
  using range=range_t<filter_psuedo_iterator<It, F>>;

  using range::begin; using range::end; using range::empty;

  filter_range( range_t<It> r, F f ):
    helper{{r}, std::forward<F>(f)},
    range{ {this, false}, {this, true} }
  {}
};

template<class F>
auto filter( F&& f ) {
    return [f=std::forward<F>(f)](auto&& r)
    {
        using std::begin; using std::end;
        using iterator = decltype(begin(r));
        return filter_range<iterator, std::decay_t<decltype(f)>>{
            range(begin(r), end(r)), f
        };
    };
};

Ho preso scorciatoie. Una vera libreria dovrebbe creare veri e propri iteratori, non il filefor(:) pseudo-fascate qualificanti che ho creato io.

Al punto di utilizzo, assomiglia a questo:

int main()
{
  std::vector<int> test = {1,2,3,4,5};
  for( auto i: filter([](auto x){return x%2;})( test ) )
    std::cout << i << '\n';
}

che è piuttosto carino e stampa

1
3
5

Esempio dal vivo .

C'è una proposta di aggiunta a C ++ chiamata Rangesv3 che fa questo tipo di cose e altro ancora. boostdispone anche di intervalli di filtri / iteratori disponibili. boost ha anche aiutanti che rendono la scrittura di quanto sopra molto più breve.


15

Uno stile che viene utilizzato abbastanza da menzionare, ma non è stato ancora menzionato, è:

for(int i=0; i<myCollection.size(); i++) {
  if (myCollection[i] != SOMETHING)
    continue;

  DoStuff();
}

vantaggi:

  • Non cambia il livello di indentazione DoStuff();quando la complessità della condizione aumenta. Logicamente, DoStuff();dovrebbe essere al livello più alto difor ciclo, e lo è.
  • Rende immediatamente chiaro che il ciclo itera sulle SOMETHINGs della raccolta, senza richiedere al lettore di verificare che non ci sia nulla dopo la chiusura }delif blocco.
  • Non richiede librerie o macro o funzioni di supporto.

svantaggi:

  • continue, come altre istruzioni di controllo del flusso, viene utilizzato in modo improprio in modi che portano a codice difficile da seguire così tanto che alcune persone si oppongono a qualsiasi uso di essi: c'è uno stile di codifica valido che alcuni seguono che evita continue, che evita breakaltro che in a switch, che evita returnaltro che alla fine di una funzione.

3
Direi che in un forciclo che corre a molte righe, un "se no, continua" di due righe è molto più chiaro, logico e leggibile. Immediatamente dicendo "salta questo se" dopo che l' foraffermazione si legge bene e, come hai detto, non fa rientrare gli aspetti funzionali rimanenti del ciclo. Se continueè più in basso, tuttavia, viene sacrificata una certa chiarezza (cioè se qualche operazione verrà sempre eseguita prima ifdell'istruzione).
anonimo

11
for(auto const &x: myCollection) if(x == something) doStuff();

forMi sembra più o meno una comprensione specifica del C ++ . A te?


Non credo che la parola chiave auto fosse presente prima di c ++ 11, quindi non direi che sia un c ++ molto classico. Se posso fare una domanda qui nel commento, "auto const" direbbe al compilatore che può riorganizzare tutti gli elementi come vuole? Forse sarà più facile per il compilatore pianificare di evitare la ramificazione se questo è il caso.
mathreadler

1
@mathreadler Prima le persone smettono di preoccuparsi del "c ++ classico", meglio è. C ++ 11 è stato un evento macroevolutivo per il linguaggio e ha 5 anni: dovrebbe essere il minimo per cui ci sforziamo. Ad ogni modo, l'OP lo ha etichettato e C ++ 14 (anche meglio!). No, auto constnon ha alcuna incidenza sull'ordine di iterazione. Se guardi in base a intervalli for, vedrai che fondamentalmente esegue un ciclo standard da begin()a end()con dereferenziazione implicita. Non c'è modo che possa infrangere le garanzie di ordinazione (se presenti) del contenitore su cui viene ripetuto; sarebbe stato deriso dalla faccia della Terra
underscore_d

1
@mathreadler, in realtà lo era, aveva solo un significato abbastanza diverso. Ciò che non era presente è range-for ... e qualsiasi altra caratteristica distinta di C ++ 11. Quello che intendevo qui era che range-fors, std::futures, std::functions, anche quelle chiusure anonime sono molto ben c ++ nella sintassi; ogni lingua ha il suo linguaggio e quando incorpora nuove caratteristiche cerca di farle imitare la vecchia ben nota sintassi.
bipll

@underscore_d, a un compilatore è consentito eseguire qualsiasi trasformazione a condizione che la regola come se fosse rispettata, non è vero?
bipll

1
Hmmm, e cosa si può intendere con questo?
bipll

7

Se DoStuff () dipendesse da me in qualche modo in futuro, allora proporrei questa variante di mascheramento bit senza rami garantita.

unsigned int times = 0;
const int kSize = sizeof(unsigned int)*8;
for(int i = 0; i < myCollection.size()/kSize; i++){
  unsigned int mask = 0;
  for (int j = 0; j<kSize; j++){
    mask |= (myCollection[i*kSize+j]==SOMETHING) << j;
  }
  times+=popcount(mask);
}

for(int i=0;i<times;i++)
   DoStuff();

Dove popcount è qualsiasi funzione che esegue un conteggio della popolazione (count numero di bit = 1). Ci sarà una certa libertà di porre vincoli più avanzati a io e ai loro vicini. Se ciò non è necessario, possiamo rimuovere il loop interno e rifare il loop esterno

for(int i = 0; i < myCollection.size(); i++)
  times += (myCollection[i]==SOMETHING);

seguito da a

for(int i=0;i<times;i++)
   DoStuff();

6

Inoltre, se non ti interessa riordinare la raccolta, std :: partition è economico.

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

void DoStuff(int i)
{
    std::cout << i << '\n';
}

int main()
{
    using namespace std::placeholders;

    std::vector<int> v {1, 2, 5, 0, 9, 5, 5};
    const int SOMETHING = 5;

    std::for_each(v.begin(),
                  std::partition(v.begin(), v.end(),
                                 std::bind(std::equal_to<int> {}, _1, SOMETHING)), // some condition
                  DoStuff); // action
}

Ma std::partitionriordina il contenitore.
celtschk

5

Sono in soggezione della complessità delle soluzioni di cui sopra. Stavo per suggerire un semplice #define foreach(a,b,c,d) for(a; b; c)if(d)ma ha alcuni evidenti deficit, ad esempio, devi ricordarti di usare virgole invece di punti e virgola nel tuo ciclo e non puoi usare l'operatore virgola in ao c.

#include <list>
#include <iostream>

using namespace std; 

#define foreach(a,b,c,d) for(a; b; c)if(d)

int main(){
  list<int> a;

  for(int i=0; i<10; i++)
    a.push_back(i);

  for(auto i=a.begin(); i!=a.end(); i++)
    if((*i)&1)
      cout << *i << ' ';
  cout << endl;

  foreach(auto i=a.begin(), i!=a.end(), i++, (*i)&1)
    cout << *i << ' ';
  cout << endl;

  return 0;
}

3
La complessità di alcune risposte è elevata solo perché mostrano prima un metodo generico riutilizzabile (che faresti solo una volta) e poi lo usano. Non efficace se si dispone di un ciclo con una condizione if nell'intera applicazione, ma molto efficace se si verifica mille volte.
gnasher729

1
Come la maggior parte dei suggerimenti, questo rende più difficile, non più facile, identificare l'intervallo e la condizione di selezione. E l'uso di una macro aumenta l'incertezza su quando (e quanto spesso) le espressioni vengono valutate, anche se qui non ci sono sorprese.
PJTraill

2

Un'altra soluzione nel caso in cui le i: s siano importanti. Questo costruisce una lista che riempie gli indici di cui chiamare doStuff (). Ancora una volta il punto principale è evitare la ramificazione e scambiarla con costi aritmetici pipelineabili.

int buffer[someSafeSize];
int cnt = 0; // counter to keep track where we are in list.
for( int i = 0; i < container.size(); i++ ){
   int lDecision = (container[i] == SOMETHING);
   buffer[cnt] = lDecision*i + (1-lDecision)*buffer[cnt];
   cnt += lDecision;
}

for( int i=0; i<cnt; i++ )
   doStuff(buffer[i]); // now we could pass the index or a pointer as an argument.

La linea "magica" è la linea di caricamento del buffer che calcola aritmeticamente se mantenere il valore e rimanere in posizione o contare la posizione e aggiungere valore. Quindi scambiamo un potenziale ramo con alcune logiche e aritmetiche e forse alcuni colpi di cache. Uno scenario tipico in cui ciò sarebbe utile è se doStuff () esegue una piccola quantità di calcoli pipeline e qualsiasi ramo tra le chiamate potrebbe interrompere tali pipeline.

Quindi basta eseguire il ciclo sul buffer ed eseguire doStuff () fino a raggiungere cnt. Questa volta avremo la corrente i memorizzata nel buffer in modo da poterla utilizzare nella chiamata a doStuff () se necessario.


1

Si può descrivere il modello di codice come l'applicazione di una funzione a un sottoinsieme di un intervallo, o in altre parole: applicarlo al risultato dell'applicazione di un filtro all'intero intervallo.

Ciò è realizzabile nel modo più semplice con la libreria Ranges -v3 di Eric Neibler ; anche se è un po 'un pugno nell'occhio, perché vuoi lavorare con gli indici:

using namespace ranges;
auto mycollection_has_something = 
    [&](std::size_t i) { return myCollection[i] == SOMETHING };
auto filtered_view = 
    views::iota(std::size_t{0}, myCollection.size()) | 
    views::filter(mycollection_has_something);
for (auto i : filtered_view) { DoStuff(); }

Ma se sei disposto a rinunciare agli indici, otterrai:

auto is_something = [&SOMETHING](const decltype(SOMETHING)& x) { return x == SOMETHING };
auto filtered_collection = myCollection | views::filter(is_something);
for (const auto& x : filtered_collection) { DoStuff(); }

che è più bello IMHO.

PS: la libreria degli intervalli sta per lo più nello standard C ++ in C ++ 20.


0

Citerò solo Mike Acton, direbbe sicuramente:

Se devi farlo, hai un problema con i tuoi dati. Ordina i tuoi dati!

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.