Perché utilizzare le funzioni di inizio e fine non membro in C ++ 11?


197

Ogni contenitore standard ha un metodo begine endper restituire iteratori per quel contenitore. Tuttavia, C ++ 11 ha apparentemente introdotto funzioni gratuite chiamate std::begine std::endche chiamano le funzioni begine endmember. Quindi, invece di scrivere

auto i = v.begin();
auto e = v.end();

tu scriveresti

auto i = std::begin(v);
auto e = std::end(v);

Nel suo discorso, Writing Modern C ++ , Herb Sutter afferma che dovresti sempre usare le funzioni gratuite ora quando vuoi l'iteratore iniziale o finale di un contenitore. Tuttavia, non approfondisce il motivo per cui si vorrebbe. Guardando il codice, ti salva tutto di un carattere. Quindi, per quanto riguarda i contenitori standard, le funzioni gratuite sembrano completamente inutili. Herb Sutter ha indicato che c'erano vantaggi per i contenitori non standard, ma ancora una volta non è entrato nei dettagli.

Quindi, la domanda è: cosa fanno esattamente le versioni delle funzioni libere std::begine std::endcosa fanno oltre a chiamare le corrispondenti versioni delle funzioni dei membri, e perché dovresti usarle?


29
È un personaggio in meno, salva quei punti per i tuoi figli: xkcd.com/297
HostileFork dice che non fidarti di SE

In qualche modo odio usarli perché dovrei ripetere std::tutto il tempo.
Michael Chourdakis,

Risposte:


162

Come si chiama .begin()e.end() su un C-array?

Le funzioni libere consentono una programmazione più generica perché possono essere aggiunte successivamente, su una struttura di dati che non è possibile modificare.


7
@JonathanMDavis: puoi avere gli endarray dichiarati staticamente ( int foo[5]) usando i trucchi di programmazione dei template. Una volta che è decaduto in un puntatore, sei ovviamente sfortunato.
Matthieu M.

33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh,

6
@JonathanMDavis: Come indicato dagli altri, è certamente possibile ottenere begine endsu un array C purché non lo si sia già decomposto in un puntatore da soli - @Huw lo spiega. Per quanto riguarda il motivo per cui vorresti: immagina di aver refactored il codice che utilizzava un array per utilizzare un vettore (o viceversa, per qualsiasi motivo). Se stai usando begine end, e forse un po 'di digitazione intelligente, il codice di implementazione non dovrà cambiare affatto (tranne forse alcuni dei typedef).
Karl Knechtel,

31
@JonathanMDavis: le matrici non sono puntatori. E per tutti: per il fine di porre fine a questa confusione sempre più evidente, smetti di riferirti a (alcuni) puntatori come "array decaduti". Non esiste una tale terminologia nella lingua e in realtà non serve a nulla. I puntatori sono puntatori, le matrici sono matrici. Le matrici possono essere convertite in un puntatore al loro primo elemento in modo implicito, ma è comunque solo un vecchio puntatore normale, senza alcuna distinzione con gli altri. Ovviamente non è possibile ottenere la "fine" di un puntatore, caso chiuso.
GManNickG

5
Bene, oltre alle matrici ci sono un gran numero di API che espongono aspetti simili al contenitore. Ovviamente non è possibile modificare un'API di terze parti ma è possibile scrivere facilmente queste funzioni di inizio / fine indipendenti.
edA-qa mort-ora-y

35

Considera il caso in cui hai una libreria che contiene classe:

class SpecialArray;

ha 2 metodi:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

per scorrere i suoi valori devi ereditare da questa classe e definire begin()e end()metodi per i casi in cui

auto i = v.begin();
auto e = v.end();

Ma se lo usi sempre

auto i = begin(v);
auto e = end(v);

Puoi farlo:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

dov'è SpecialArrayIteratorqualcosa come:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

ora ie epuò essere legalmente utilizzato per l'iterazione e l'accesso ai valori di SpecialArray


8
Questo non dovrebbe includere le template<>linee. Stai dichiarando un nuovo sovraccarico di funzioni, non specializzando un modello.
David Stone,

33

Utilizzando il begineend funzioni free aggiunge uno strato di indiretta. Di solito ciò viene fatto per consentire una maggiore flessibilità.

In questo caso posso pensare ad alcuni usi.

L'uso più ovvio è per gli array C (non i puntatori c).

Un altro è quando si tenta di utilizzare un algoritmo standard su un contenitore non conforme (ovvero nel contenitore manca un .begin()metodo). Supponendo che non si possa semplicemente riparare il contenitore, la migliore opzione successiva è quella di sovraccaricare la beginfunzione. Herb suggerisce di utilizzare sempre la beginfunzione per promuovere l'uniformità e la coerenza nel codice. Invece di dover ricordare quali contenitori supportano il metodo begine quali necessitano di funzione begin.

Per inciso, la prossima C ++ rev dovrebbe copiare D's notazione pseudo-utente . Se a.foo(b,c,d)non è definito, tenta invece foo(a,b,c,d). È solo un po 'di zucchero sintattico per aiutare noi poveri umani che preferiscono la materia e l'ordinamento dei verbi.


5
La notazione pseudo-membro sembra C # /. Metodi di estensione della rete . Sono utili in varie situazioni, tuttavia - come tutte le funzioni - possono essere soggetti a "abusi".
Gareth Wilson,

5
La notazione pseudo-membro è un vantaggio per la codifica con Intellisense; colpire "a". mostra i verbi rilevanti, liberando il potere del cervello dalle liste di memorizzazione e aiutando a scoprire le funzioni API pertinenti può aiutare a prevenire la duplicazione della funzionalità, senza dover inserire le funzioni non membro nelle classi.
Matt Curtis,

Ci sono proposte per farlo in C ++, che usano il termine Unified Function Call Syntax (UFCS).
underscore_d

17

Per rispondere alla tua domanda, le funzioni gratuite begin () e end () per impostazione predefinita non fanno altro che chiamare le funzioni .begin () e .end () del membro del contenitore. Da <iterator>, incluso automaticamente quando si utilizza uno qualsiasi dei contenitori standard come <vector>, <list>e così via, si ottiene:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

La seconda parte della domanda è perché preferire le funzioni libere se tutto ciò che fanno è chiamare comunque le funzioni membro. Dipende molto dal tipo di oggetto vpresente nel codice di esempio. Se il tipo di v è un tipo di contenitore standard, come vector<T> v;allora non importa se usi le funzioni free o member, fanno la stessa cosa. Se il tuo oggetto vè più generico, come nel seguente codice:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Quindi l'utilizzo delle funzioni membro interrompe il codice per matrici T = C, stringhe C, enum, ecc. Utilizzando le funzioni non membro, si pubblicizza un'interfaccia più generica che le persone possono facilmente estendere. Utilizzando l'interfaccia delle funzioni libere:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Il codice ora funziona con le matrici T = C e le stringhe C. Ora scrivendo una piccola quantità di codice adattatore:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Possiamo ottenere che il tuo codice sia compatibile anche con enumerazioni iterabili. Penso che il punto principale di Herb sia che usare le funzioni gratuite sia facile come usare le funzioni membro, e che dia al tuo codice la retrocompatibilità con i tipi di sequenza C e la compatibilità con tipi di sequenza non-stl (e tipi future-stl!), a basso costo per altri sviluppatori.


Bei esempi. Non prenderei uno enumo qualsiasi altro tipo fondamentale come riferimento, però; saranno più economici da copiare rispetto a quelli indiretti.
underscore_d

6

Uno dei vantaggi std::begine std::endè che servono come punti di estensione per l'implementazione dell'interfaccia standard per le classi esterne.

Se desideri utilizzare la CustomContainerclasse con intervallo per la funzione loop o template che prevede .begin()e .end()metodi, dovresti ovviamente implementare quei metodi.

Se la classe fornisce questi metodi, non è un problema. Altrimenti, dovresti modificarlo *.

Ciò non è sempre possibile, ad esempio quando si utilizza una libreria esterna, specialmente commerciale e di tipo chiuso.

In tali situazioni, std::begine std::endtornare utile, dal momento che è possibile fornire API iteratore senza modificare la classe stessa, ma piuttosto sovraccaricare le funzioni gratuite.

Esempio: supponiamo che desideri implementare la count_iffunzione che accetta un contenitore anziché una coppia di iteratori. Tale codice potrebbe apparire così:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

Ora, per qualsiasi classe che desideri utilizzare con questa personalizzazione count_if, devi solo aggiungere due funzioni gratuite, invece di modificare quelle classi.

Ora, C ++ ha un meccanismo chiamato Argument Dependent Lookup (ADL), che rende tale approccio ancora più flessibile.

In breve, ADL significa che quando un compilatore risolve una funzione non qualificata (ovvero una funzione senza spazio dei nomi, come al beginposto di std::begin), prenderà in considerazione anche le funzioni dichiarate negli spazi dei nomi dei suoi argomenti. Per esempio:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

In questo caso, non importa quali siano i nomi qualificati some_lib::begine some_lib::end , poiché lo CustomContainerè some_lib::anche, il compilatore utilizzerà tali sovraccarichicount_if .

Questo è anche il motivo per avere using std::begin;e using std::end;entrare count_if. Questo ci consente di utilizzare non qualificati begine end, quindi, consentire ADL e consentire al compilatore di scegliere std::begine std::endquando non vengono trovate altre alternative.

Possiamo mangiare il cookie e avere il cookie, ovvero avere un modo per fornire l'implementazione personalizzata di begin/ endmentre il compilatore può tornare a quelli standard.

Alcune note:

  • Per lo stesso motivo, ci sono altre funzioni simili: std::rbegin/ rend, std::sizee std::data.

  • Come menzionano altre risposte, le std::versioni presentano sovraccarichi per array nudi. È utile, ma è semplicemente un caso speciale di quello che ho descritto sopra.

  • Usare std::begine gli amici è una buona idea quando si scrive un codice modello, perché questo rende questi modelli più generici. Per i non-template potresti anche usare i metodi, quando applicabile.

PS Sono consapevole che questo post ha quasi 7 anni. Mi sono imbattuto perché volevo rispondere a una domanda contrassegnata come duplicata e ho scoperto che nessuna risposta menziona ADL.


Buona risposta, in particolare spiegando apertamente l'ADL, piuttosto che lasciarlo all'immaginazione come tutti gli altri, anche quando lo stavano mostrando in azione!
underscore_d

5

Considerando che le funzioni non membri non offrono alcun vantaggio per i contenitori standard, il loro utilizzo applica uno stile più coerente e flessibile. Se in qualche momento desideri estendere una classe contenitore non standard esistente, preferiresti definire sovraccarichi delle funzioni libere, invece di alterare la definizione della classe esistente. Quindi per i contenitori non standard sono molto utili e l'utilizzo sempre delle funzioni gratuite rende il codice più flessibile in quanto è possibile sostituire il contenitore standard con un contenitore non standard più facilmente e il tipo di contenitore sottostante è più trasparente al codice in quanto supporta una più ampia varietà di implementazioni di container.

Ma ovviamente questo deve sempre essere ponderato correttamente e anche l'astrazione non va bene. Anche se l'utilizzo delle funzioni gratuite non è un eccesso di astrazione, ciò nonostante interrompe la compatibilità con il codice C ++ 03, che a questa giovane età di C ++ 11 potrebbe essere ancora un problema per te.


3
In C ++ 03, puoi semplicemente usare boost::begin()/ end(), quindi non c'è vera incompatibilità :)
Marc Mutz - mmutz

1
@ MarcMutz-mmutz Bene, aumentare la dipendenza non è sempre un'opzione (ed è piuttosto eccessivo se usato solo per begin/end). Quindi riterrei incompatibile anche con il C ++ 03 puro. Ma come detto, è un'incompatibilità piuttosto piccola (e sempre più piccola), poiché C ++ 11 (almeno begin/endin particolare) sta ottenendo sempre più adozione, comunque.
Christian Rau,

0

In definitiva il vantaggio è nel codice che è generalizzato in modo tale da essere indipendente dal contenitore. Può operare su un std::vector, un array o un intervallo senza modifiche al codice stesso.

Inoltre, i container, anche i container non di proprietà, possono essere adattati in modo tale da poter essere utilizzati agnosticamente anche tramite codice utilizzando accessori basati su range non membri.

Vedi qui per maggiori dettagli.

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.