Warum Start- und Endfunktionen von Nichtmitgliedern in C ++ 11 verwenden?

196

Jeder Standard - Container hat ein beginund endVerfahren für Iteratoren für diesen Behälter zurückkehrt. C ++ 11 hat jedoch anscheinend freie Funktionen eingeführt, die aufgerufen werden std::beginund std::enddie die Funktionen beginund endmember aufrufen . Also anstatt zu schreiben

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

du würdest schreiben

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

In seinem Vortrag Writing Modern C ++ sagt Herb Sutter, dass Sie die freien Funktionen jetzt immer verwenden sollten, wenn Sie den Start- oder Enditerator für einen Container möchten. Er geht jedoch nicht ins Detail, warum Sie möchten. Wenn Sie sich den Code ansehen, sparen Sie alle Zeichen. Was die Standardcontainer angeht, scheinen die freien Funktionen völlig nutzlos zu sein. Herb Sutter gab an, dass es Vorteile für nicht standardmäßige Behälter gibt, aber er ging auch hier nicht ins Detail.

Die Frage ist also, was genau die kostenlosen Funktionsversionen von std::beginund std::endüber das Aufrufen der entsprechenden Mitgliedsfunktionsversionen hinaus tun und warum Sie sie verwenden möchten.

Jonathan M Davis
quelle
29
Es ist ein Charakter weniger, speichere diese Punkte für deine Kinder: xkcd.com/297
HostileFork sagt, vertraue SE
Ich würde es irgendwie hassen, sie zu benutzen, weil ich die std::ganze Zeit wiederholen müsste.
Michael Chourdakis

Antworten:

161

Wie nennst du .begin()und .end()auf einem C-Array?

Freie Funktionen ermöglichen eine allgemeinere Programmierung, da sie anschließend in einer Datenstruktur hinzugefügt werden können, die Sie nicht ändern können.

Matthieu M.
quelle
7
@JonathanMDavis: Sie können die endfür statisch deklarierte Arrays ( int foo[5]) mithilfe von Tricks zur Vorlagenprogrammierung verwenden. Sobald es zu einem Zeiger verfallen ist, haben Sie natürlich kein Glück mehr.
Matthieu M.
33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh
6
@JonathanMDavis: Wie die anderen angedeutet, ist es sicherlich möglich zu erhalten beginund endein C - Array so lange auf , wie Sie nicht bereits auf einen Zeiger abgeklungen sind , sich - @Huw buchstabiert es heraus. Warum Sie möchten: Stellen Sie sich vor, Sie hätten Code überarbeitet, der ein Array zur Verwendung eines Vektors verwendet (oder umgekehrt, aus welchem ​​Grund auch immer). Wenn Sie beginund endund möglicherweise ein cleveres Typedeffing verwendet haben, muss sich der Implementierungscode überhaupt nicht ändern (außer vielleicht einigen Typedefs).
Karl Knechtel
31
@ JonathanMDavis: Arrays sind keine Zeiger. Und für alle: Um diese immer größere Verwirrung zu beenden, sollten Sie aufhören, (einige) Zeiger als "verfallene Arrays" zu bezeichnen. Es gibt keine solche Terminologie in der Sprache und es gibt wirklich keine Verwendung dafür. Zeiger sind Zeiger, Arrays sind Arrays. Arrays können implizit in einen Zeiger auf ihr erstes Element konvertiert werden, aber das ist immer noch nur ein normaler alter Zeiger, ohne Unterschied zu anderen. Natürlich kann man das "Ende" eines Zeigers nicht bekommen, wenn der Fall geschlossen ist.
GManNickG
5
Abgesehen von Arrays gibt es eine große Anzahl von APIs, die containerähnliche Aspekte offenlegen. Natürlich können Sie eine Drittanbieter-API nicht ändern, aber Sie können diese freistehenden Start- / Endfunktionen problemlos schreiben.
edA-qa mort-ora-y
35

Betrachten Sie den Fall, wenn Sie eine Bibliothek haben, die eine Klasse enthält:

class SpecialArray;

Es gibt zwei Methoden:

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

Um über die Werte zu iterieren, müssen Sie von dieser Klasse erben begin()und end()Methoden für Fälle definieren, in denen

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

Aber wenn Sie immer verwenden

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

du kannst das:

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

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

wo SpecialArrayIteratorist so etwas wie:

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
};

Jetzt iund ekann legal für die Iteration und den Zugriff auf Werte von SpecialArray verwendet werden

GreenScape
quelle
8
Dies sollte die template<>Zeilen nicht enthalten . Sie deklarieren eine neue Funktionsüberladung und spezialisieren keine Vorlage.
David Stone
33

Durch die Verwendung der Funktionen beginund endfree wird eine Indirektionsebene hinzugefügt. Normalerweise wird dies getan, um mehr Flexibilität zu ermöglichen.

In diesem Fall kann ich mir einige Verwendungszwecke vorstellen.

Die naheliegendste Verwendung sind C-Arrays (keine C-Zeiger).

Eine andere Möglichkeit besteht darin, einen Standardalgorithmus für einen nicht konformen Container zu verwenden (dh dem Container fehlt eine .begin()Methode). Angenommen, Sie können den Container nicht einfach reparieren, besteht die nächstbeste Option darin, die beginFunktion zu überladen . Herb schlägt vor, dass Sie die beginFunktion immer verwenden , um die Einheitlichkeit und Konsistenz Ihres Codes zu fördern. Anstatt sich merken zu müssen, welche Container die Methode unterstützen beginund welche Funktion benötigen begin.

Abgesehen davon sollte die nächste C ++ - Version die Pseudo-Member-Notation von D kopieren . Wenn a.foo(b,c,d)nicht definiert, wird es stattdessen versucht foo(a,b,c,d). Es ist nur ein wenig syntaktischer Zucker, um uns armen Menschen zu helfen, die lieber ein Thema als eine Verbreihenfolge bevorzugen.

deft_code
quelle
5
Die Pseudo-Member-Notation sieht aus wie C # /. Net- Erweiterungsmethoden . Sie sind jedoch für verschiedene Situationen nützlich, können jedoch - wie alle Funktionen - anfällig für Missbrauch sein.
Gareth Wilson
5
Die Pseudo-Member-Notation ist ein Segen für die Codierung mit Intellisense. "a." Zeigt relevante Verben an, befreit die Gehirnleistung vom Speichern von Listen und hilft beim Erkennen relevanter API-Funktionen, um das Duplizieren von Funktionen zu verhindern, ohne dass Funktionen, die keine Mitglieder sind, in Klassen eingeteilt werden müssen.
Matt Curtis
Es gibt Vorschläge, dies in C ++ zu integrieren, die den Begriff Unified Function Call Syntax (UFCS) verwenden.
underscore_d
17

Um Ihre Frage zu beantworten, rufen die freien Funktionen begin () und end () standardmäßig nur die Mitgliedsfunktionen .begin () und .end () des Containers auf. Von <iterator>automatisch enthalten , wenn Sie eine der Standard - Container verwenden wie <vector>, <list>etc., erhalten Sie:

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

Der zweite Teil Ihrer Frage ist, warum Sie die freien Funktionen bevorzugen, wenn sie ohnehin nur die Mitgliedsfunktionen aufrufen. Das hängt wirklich davon ab, welche Art von Objekt vsich in Ihrem Beispielcode befindet. Wenn der Typ von v ein Standardcontainertyp ist, spielt vector<T> v;es keine Rolle, ob Sie die Funktionen free oder member verwenden, sie tun dasselbe. Wenn Ihr Objekt vallgemeiner ist, wie im folgenden Code:

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

Wenn Sie dann die Elementfunktionen verwenden, wird Ihr Code für T = C-Arrays, C-Zeichenfolgen, Aufzählungen usw. unterbrochen. Durch die Verwendung der Nicht-Elementfunktionen kündigen Sie eine allgemeinere Schnittstelle an, die von Personen problemlos erweitert werden kann. Mit der freien Funktionsschnittstelle:

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

Der Code funktioniert jetzt mit T = C-Arrays und C-Strings. Schreiben Sie nun eine kleine Menge Adaptercode:

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); }

Wir können Ihren Code auch mit iterierbaren Aufzählungen kompatibel machen. Ich denke, Herbs Hauptpunkt ist, dass die Verwendung der freien Funktionen genauso einfach ist wie die Verwendung der Mitgliedsfunktionen, und dass Ihr Code abwärtskompatibel mit C-Sequenztypen und vorwärtskompatibel mit Nicht-STL-Sequenztypen (und Future-STL-Typen!) Ist. mit geringen Kosten für andere Entwickler.

Nate
quelle
Schöne Beispiele. Ich würde jedoch keinen enumoder einen anderen grundlegenden Typ als Referenz nehmen; Sie sind billiger zu kopieren als indirekt.
underscore_d
6

Ein Vorteil von std::begin und std::endist, dass sie als Erweiterungspunkte für die Implementierung einer Standardschnittstelle für externe Klassen dienen.

Wenn Sie eine CustomContainerKlasse mit bereichsbasierter for-Schleife oder Vorlagenfunktion verwenden möchten, die .begin()und erwartet.end() Methoden verwendet, müssen Sie diese Methoden natürlich implementieren.

Wenn die Klasse diese Methoden bereitstellt, ist dies kein Problem. Wenn dies nicht der Fall ist, müssen Sie es ändern *.

Dies ist beispielsweise bei Verwendung einer externen Bibliothek, insbesondere einer kommerziellen und einer Closed-Source-Bibliothek, nicht immer möglich.

In solchen Situationen std::beginundstd::end nützlich, da man eine Iterator-API bereitstellen kann, ohne die Klasse selbst zu ändern, sondern freie Funktionen zu überladen.

Beispiel: Angenommen, Sie möchten eine count_ifFunktion implementieren , die einen Container anstelle eines Paares von Iteratoren verwendet. Ein solcher Code könnte folgendermaßen aussehen:

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));
}

Jetzt müssen Sie für jede Klasse, die Sie mit dieser benutzerdefinierten Klasse verwenden möchten count_if, nur zwei kostenlose Funktionen hinzufügen, anstatt diese Klassen zu ändern.

Jetzt verfügt C ++ über einen Mechanismus namens Argument Dependent Lookup (ADL), der diesen Ansatz noch flexibler macht.

Kurz gesagt bedeutet ADL, dass ein Compiler, wenn er eine nicht qualifizierte Funktion auflöst (dh eine Funktion ohne Namespace, wie beginanstelle von std::begin), auch Funktionen berücksichtigt, die in Namespaces seiner Argumente deklariert sind. Beispielsweise:

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 diesem Fall spielt es keine Rolle, ob qualifizierte Namen vorhanden sind, some_lib::beginund some_lib::end - da dies auch der Fall CustomContainerist some_lib::, verwendet der Compiler diese Überladungen incount_if .

Das ist auch der Grund für using std::begin;und using std::end;in count_if. Dies ermöglicht es uns, unqualifiziert zu verwenden beginund endsomit ADL zuzulassen und dem Compiler die Auswahl zu ermöglichen, std::beginund std::endwenn keine anderen Alternativen gefunden werden.

Wir können den Cookie essen und den Cookie haben - dh eine Möglichkeit haben, eine benutzerdefinierte Implementierung von begin/ bereitzustellen, endwährend der Compiler auf Standard- Cookies zurückgreifen kann.

Einige Notizen:

  • Aus dem gleichen Grund gibt es andere ähnliche Funktionen: std::rbegin/ rend, std::sizeund std::data.

  • Wie in anderen Antworten erwähnt, std::weisen Versionen Überladungen für nackte Arrays auf. Das ist nützlich, aber es ist einfach ein Sonderfall dessen, was ich oben beschrieben habe.

  • Die Verwendung von std::beginund Freunden ist besonders beim Schreiben von Vorlagencode eine gute Idee, da diese Vorlagen dadurch allgemeiner werden. Für Nicht-Vorlagen können Sie gegebenenfalls auch Methoden verwenden.

PS Mir ist bewusst, dass dieser Beitrag fast 7 Jahre alt ist. Ich bin darauf gestoßen, weil ich eine Frage beantworten wollte, die als Duplikat markiert war, und festgestellt habe, dass hier keine Antwort ADL erwähnt.

joe_chip
quelle
Gute Antwort, insbesondere ADL offen zu erklären, anstatt es der Fantasie zu überlassen, wie es alle anderen taten - selbst wenn sie es in Aktion zeigten!
underscore_d
5

Während die Nichtmitgliedsfunktionen für die Standardcontainer keinen Vorteil bieten, erzwingt ihre Verwendung einen konsistenteren und flexibleren Stil. Wenn Sie zu einem bestimmten Zeitpunkt eine vorhandene Nicht-Standard-Containerklasse erweitern möchten, möchten Sie lieber Überladungen der freien Funktionen definieren, anstatt die Definition der vorhandenen Klasse zu ändern. Für Nicht-Standard-Container sind sie daher sehr nützlich. Wenn Sie immer die kostenlosen Funktionen verwenden, wird Ihr Code flexibler, da Sie den Standard-Container einfacher durch einen Nicht-Standard-Container ersetzen können und der zugrunde liegende Containertyp für Ihren Code transparenter ist unterstützt eine viel größere Vielfalt von Container-Implementierungen.

Aber das muss natürlich immer richtig gewichtet werden und Überabstraktion ist auch nicht gut. Die Verwendung der kostenlosen Funktionen ist zwar keine große Überabstraktion, bricht jedoch die Kompatibilität mit C ++ 03-Code, was in diesem jungen Alter von C ++ 11 möglicherweise immer noch ein Problem für Sie darstellt.

Christian Rau
quelle
3
In C ++ 03 können Sie einfach boost::begin()/ verwenden end(), damit es keine wirkliche Inkompatibilität gibt :)
Marc Mutz - mmutz
1
@ MarcMutz-mmutz Nun, Boost-Abhängigkeit ist nicht immer eine Option (und ein ziemlicher Overkill, wenn sie nur für verwendet wird begin/end). Daher würde ich das auch als Inkompatibilität mit reinem C ++ 03 betrachten. Aber wie gesagt, es ist eine eher kleine (und immer kleinere) Inkompatibilität, da C ++ 11 (zumindest begin/endinsbesondere) sowieso immer mehr Akzeptanz findet.
Christian Rau
0

Letztendlich liegt der Vorteil in Code, der so verallgemeinert ist, dass er containerunabhängig ist. Es kann mit einem std::vector, einem Array oder einem Bereich arbeiten, ohne den Code selbst zu ändern.

Darüber hinaus können Container, auch nicht im Besitz befindliche Container, so nachgerüstet werden, dass sie auch agnostisch durch Code unter Verwendung von Accessoren verwendet werden können, die nicht auf Mitgliederbereichen basieren.

Sehen Sie hier für weitere Details.

Jonathan Mee
quelle