Dlaczego warto korzystać z funkcji początkowych i końcowych innych niż członkowie w C ++ 11?

197

Każdy standardowy kontener ma metodę begini enddo zwracania iteratorów dla tego kontenera. Jednak C ++ 11 najwyraźniej wprowadził wolne funkcje o nazwie std::begini std::endktóre wywołują funkcje begini end. Zamiast pisać

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

piszesz

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

W swoim wystąpieniu, Writing Modern C ++ , Herb Sutter mówi, że powinieneś zawsze korzystać z darmowych funkcji teraz, gdy chcesz rozpocząć lub zakończyć iterator dla kontenera. Nie wyjaśnia on jednak szczegółowo, dlaczego chcesz. Patrząc na kod, oszczędza ci to jednego znaku. Jeśli chodzi o standardowe kontenery, darmowe funkcje wydają się całkowicie bezużyteczne. Herb Sutter wskazał, że istnieją niestandardowe pojemniki, ale ponownie nie wdał się w szczegóły.

Pytanie brzmi: co dokładnie robią wolne wersje funkcji std::begini std::endpoza wywoływaniem ich odpowiednich wersji funkcji członkowskich i dlaczego chcesz ich używać?

Jonathan M. Davis
źródło
29
To o jeden znak mniej, zapisz te kropki dla swoich dzieci: xkcd.com/297
HostileFork mówi: nie ufaj
Jakoś nie chciałbym ich używać, ponieważ musiałbym std::cały czas powtarzać .
Michael Chourdakis

Odpowiedzi:

162

Jak dzwonisz .begin()i .end()macierz C?

Bezpłatne funkcje pozwalają na bardziej ogólne programowanie, ponieważ można je później dodać, w strukturze danych, której nie można zmienić.

Matthieu M.
źródło
7
@JonathanMDavis: możesz mieć endtablice dla deklarowanych statycznie ( int foo[5]) przy użyciu sztuczek programistycznych. Gdy już rozpadnie się na wskaźnik, oczywiście nie masz szczęścia.
Matthieu M.,
33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh
6
@JonathanMDavis: Jak wskazano inni, to z pewnością można się dostać begini endna tablicy C, o ile nie zostało to zepsute go do wskaźnika siebie - @Huw zapis przyjmuje postać go. Co do tego, dlaczego chcesz: wyobraź sobie, że dokonałeś refaktoryzacji kodu, który używał tablicy, aby użyć wektora (lub odwrotnie, z jakiegokolwiek powodu). Jeśli używasz begini endbyć może sprytnego typowania, kod implementacyjny nie będzie musiał się w ogóle zmieniać (z wyjątkiem niektórych typów typedef).
Karl Knechtel
31
@JonathanMDavis: Tablice nie są wskaźnikami. I dla wszystkich: Aby zakończyć to zawsze widoczne zamieszanie, przestańcie odnosić się do (niektórych) wskaźników jako „zepsutych tablic”. W języku nie ma takiej terminologii i naprawdę nie ma z niej pożytku. Wskaźniki są wskaźnikami, tablice są tablicami. Tablice można pośrednio przekonwertować na wskaźnik do pierwszego elementu, ale nadal jest to zwykły stary wskaźnik, bez rozróżnienia na inne. Oczywiście nie można uzyskać „końca” wskaźnika, obudowa jest zamknięta.
GManNickG
5
Cóż, poza tablicami istnieje wiele interfejsów API, które ujawniają aspekty podobne do kontenerów. Oczywiście nie możesz modyfikować interfejsu API innej firmy, ale możesz łatwo napisać te wolno stojące funkcje początkowe / końcowe.
edA-qa mort-ora-y
35

Rozważ przypadek, gdy masz bibliotekę zawierającą klasę:

class SpecialArray;

ma 2 metody:

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

aby iterować po jego wartościach, musisz odziedziczyć po tej klasie i zdefiniować begin()oraz end()metody dla przypadków kiedy

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

Ale jeśli zawsze używasz

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

możesz to zrobić:

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

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

gdzie SpecialArrayIteratorjest coś takiego:

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

teraz ii emogą być legalnie używane do iteracji i uzyskiwania dostępu do wartości SpecialArray

GreenScape
źródło
8
Nie powinno to obejmować template<>wierszy. Zadeklarujesz przeciążenie nowej funkcji, nie specjalizując się w szablonie.
David Stone
33

Używanie begini endbezpłatne funkcje dodaje jedną warstwę zadnie. Zwykle odbywa się to w celu zapewnienia większej elastyczności.

W tym przypadku mogę wymyślić kilka zastosowań.

Najbardziej oczywistym zastosowaniem są tablice C (nie wskaźniki C).

Innym jest próba użycia standardowego algorytmu na niezgodnym kontenerze (tzn. W kontenerze brakuje .begin()metody). Zakładając, że nie możesz po prostu naprawić kontenera, następną najlepszą opcją jest przeciążenie beginfunkcji. Herb sugeruje, abyś zawsze używał tej beginfunkcji do promowania jednolitości i spójności kodu. Zamiast pamiętać, które kontenery obsługują metodę begini które wymagają funkcji begin.

Jak z boku, w kolejnym C ++ obr należy skopiować D's zapisu pseudo użytkownika . Jeśli a.foo(b,c,d)nie jest zdefiniowane, zamiast tego próbuje foo(a,b,c,d). To tylko niewielki cukier składniowy, który pomaga nam biednym ludziom, którzy wolą sortowanie według podmiotu niż czasownika.

deft_code
źródło
5
W notacji pseudo-członek wygląda jak C # /. Net rozszerzenie metod . Są przydatne w różnych sytuacjach, jednak - jak wszystkie funkcje - mogą być podatne na „nadużycia”.
Gareth Wilson
5
Notacja pseudo-członka jest dobrodziejstwem dla kodowania za pomocą Intellisense; uderzenie „a.” pokazuje odpowiednie czasowniki, uwalniając moc mózgu z zapamiętywania list i pomagając odkryć odpowiednie funkcje API mogą pomóc w zapobieganiu powielaniu funkcji, bez konieczności dzielenia funkcji nie będących członkami na klasy.
Matt Curtis
Istnieją propozycje przeniesienia tego do C ++, które używają terminu Unified Function Call Syntax (UFCS).
underscore_d
17

Aby odpowiedzieć na twoje pytanie, darmowe funkcje start () i end () domyślnie wykonują jedynie wywołanie funkcji członka .begin () i .end () kontenera. Od <iterator>, włączone automatycznie podczas korzystania dowolny ze standardowych pojemnikach, takich jak <vector>, <list>itd, można uzyskać:

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

Drugą częścią twojego pytania jest to, dlaczego wolisz funkcje bezpłatne, jeśli i tak wywołują funkcje członka. To naprawdę zależy od tego, jaki obiekt vznajduje się w twoim przykładowym kodzie. Jeśli typ v jest standardowym typem kontenera, tak jak vector<T> v;wtedy, nie ma znaczenia, czy korzystasz z funkcji darmowej czy składowej, robią to samo. Jeśli Twój obiekt vjest bardziej ogólny, jak w poniższym kodzie:

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

Następnie użycie funkcji składowych powoduje uszkodzenie kodu dla tablic T = C, ciągów C, wyliczeń itp. Korzystając z funkcji nie będących członkami, reklamujesz bardziej ogólny interfejs, który ludzie mogą łatwo rozszerzyć. Korzystając z darmowego interfejsu funkcji:

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

Kod działa teraz z tablicami T = C i ciągami C. Teraz piszę niewielką ilość kodu adaptera:

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

Możemy sprawić, że Twój kod będzie również zgodny z iterowalnymi wyliczeniami. Myślę, że głównym punktem Herb jest to, że korzystanie z darmowych funkcji jest tak samo łatwe, jak korzystanie z funkcji składowych, i zapewnia twojemu kodowi wsteczną kompatybilność z typami sekwencji C i kompatybilność z innymi typami sekwencji nie stl (i przyszłymi typami stl!), przy niskich kosztach dla innych programistów.

Nate
źródło
Ładne przykłady. Nie wziąłbym jednak enumżadnego innego podstawowego typu przez odniesienie; kopiowanie będzie tańsze niż pośrednie.
underscore_d
6

Jedną z zalet std::begini std::endjest to, że służą one jako punkty rozszerzeń dla realizacji standardowy interfejs dla klas zewnętrznych.

Jeśli chcesz użyć CustomContainerklasy z funkcją pętli lub funkcji szablonu opartą na zakresie, która oczekuje .begin()i .end()metodami, oczywiście musisz zaimplementować te metody.

Jeśli klasa udostępnia te metody, nie stanowi to problemu. Jeśli nie, musisz go zmodyfikować *.

Nie zawsze jest to możliwe, na przykład w przypadku korzystania z biblioteki zewnętrznej, szczególnie komercyjnej i zamkniętej.

W takich sytuacjach std::begini std::endprzydają się, ponieważ można zapewnić interfejs API iteratora bez modyfikowania samej klasy, ale raczej przeciążając wolne funkcje.

Przykład: załóżmy, że chcesz zaimplementować count_iffunkcję, która pobiera kontener zamiast pary iteratorów. Taki kod może wyglądać następująco:

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

Teraz dla każdej klasy, której chcesz używać z tym niestandardem count_if, musisz tylko dodać dwie bezpłatne funkcje, zamiast modyfikować te klasy.

Teraz C ++ ma mechanisim o nazwie Argument Dependent Lookup (ADL), co czyni takie podejście jeszcze bardziej elastycznym.

W skrócie, ADL oznacza, że ​​gdy kompilator rozpozna funkcję niekwalifikowaną (tj. Funkcję bez przestrzeni nazw, jak beginzamiast zamiast std::begin), to również rozważy funkcje zadeklarowane w przestrzeni nazw swoich argumentów. Na przykład:

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

W tym przypadku nie ma znaczenia, że ​​są to kwalifikowane nazwy some_lib::begini some_lib::end - ponieważ CustomContainerjest some_lib::również w, kompilator użyje tych przeciążeń w count_if.

To również powód, dla posiadania using std::begin;i using std::end;w count_if. To pozwala nam korzystać z niewykwalifikowanych, begina endtym samym pozwalając na ADL i pozwalając kompilatorowi wybierać std::begini std::endkiedy nie ma innych alternatyw.

Możemy zjeść ciasteczko i mieć ciasteczko - tj. Mieć sposób na zapewnienie niestandardowej implementacji begin/ endpodczas gdy kompilator może wrócić do standardowych.

Niektóre uwagi:

  • Z tego samego powodu, istnieją inne podobne funkcje: std::rbegin/ rend, std::sizea std::data.

  • Jak wspominają inne odpowiedzi, std::wersje mają przeciążenia dla nagich tablic. Jest to przydatne, ale jest po prostu szczególnym przypadkiem tego, co opisałem powyżej.

  • Korzystanie std::beginze znajomych i przyjaciół jest szczególnie dobrym pomysłem podczas pisania kodu szablonu, ponieważ dzięki temu szablony są bardziej ogólne. W przypadku innych niż szablon równie dobrze możesz użyć metod, jeśli mają zastosowanie.

PS Wiem, że ten post ma prawie 7 lat. Natknąłem się na to, ponieważ chciałem odpowiedzieć na pytanie oznaczone jako duplikat i odkryłem, że w żadnej odpowiedzi nie ma mowy o ADL.

joe_chip
źródło
Dobra odpowiedź, zwłaszcza bezpośrednie wyjaśnienie ADL, zamiast pozostawiania go wyobraźni, tak jak wszyscy inni - nawet gdy pokazali to w akcji!
underscore_d
5

Podczas gdy funkcje nie będące członkami nie zapewniają żadnych korzyści dla standardowych kontenerów, użycie ich wymusza bardziej spójny i elastyczny styl. Jeśli chcesz w pewnym momencie rozszerzyć istniejącą niestabilną klasę kontenera, wolisz zdefiniować przeciążenia wolnych funkcji, zamiast zmieniać definicję istniejącej klasy. Dlatego w przypadku kontenerów innych niż standardowe są one bardzo przydatne i zawsze dzięki bezpłatnym funkcjom kod staje się bardziej elastyczny, ponieważ można łatwiej zastąpić standardowy kontener innym niż standardowy, a podstawowy typ kontenera jest bardziej przejrzysty dla kodu, ponieważ obsługuje znacznie szerszą gamę implementacji kontenerów.

Ale oczywiście zawsze musi to być odpowiednio ważone, a nadmierna abstrakcja też nie jest dobra. Chociaż korzystanie z darmowych funkcji nie jest nadmierną abstrakcją, to jednak łamie kompatybilność z kodem C ++ 03, co w młodym wieku C ++ 11 może nadal stanowić dla ciebie problem.

Christian Rau
źródło
3
W C ++ 03 możesz po prostu użyć boost::begin()/ end(), więc nie ma prawdziwej niezgodności :)
Marc Mutz - mmutz
1
@ MarcMutz-mmutz Cóż, zwiększenie zależności nie zawsze jest opcją (i jest dość przesadą, jeśli jest używane tylko begin/end). Uważałbym więc, że jest to również niezgodność z czystym C ++ 03. Ale, jak powiedziano, jest to raczej niewielka (i coraz mniejsza) niezgodność, ponieważ C ++ 11 (przynajmniej begin/endw szczególności) jest coraz bardziej adoptowany.
Christian Rau,
0

Ostatecznie korzyść tkwi w kodzie uogólnionym tak, że jest on niezależny od kontenera. Może działać na std::vectortablicy, tablicy lub zakresie bez zmian w samym kodzie.

Dodatkowo kontenery, nawet kontenery nie będące własnością, mogą być modernizowane w taki sposób, że mogą być również używane agnostycznie za pomocą kodu przy użyciu akcesoriów nie opartych na zakresie członów.

Zobacz tutaj po więcej szczegółów.

Jonathan Mee
źródło