Czy niestandardowe kontenery powinny mieć darmowe funkcje begin/end?

Podczas tworzenia niestandardowej klasy kontenera, która działa według zwykłych reguł (tzn. działa z algorytmami STL, działa z dobrze zachowanym kodem generycznym, itp.), w C++03 wystarczyło zaimplementować obsługę iteratora oraz funkcje begin/end.

C++11 wprowadza dwie nowe koncepcje-range-based for loop oraz std:: begin / end. Zakres-based for loop rozumie funkcje rozpoczynające/kończące członków, więc wszelkie kontenery C++03 obsługują zakres-based for out of the box. Dla algorytmów zalecany sposób (zgodnie z "Writing modern C++ code" autorstwa Herba Suttera) jest użycie std:: begin zamiast funkcji member.

Jednak w tym momencie muszę zapytać - czy zalecanym sposobem jest wywołanie w pełni kwalifikowanej funkcji begin () (tj. std::begin (c)) lub poleganie na ADL i wywołanie begin (c)?

ADL wydaje się bezużyteczny w tym konkretnym przypadku - ponieważ STD::begin(c) deleguje do C.begin() jeśli to możliwe, zwykłe korzyści z ADL nie wydają się mieć zastosowania. A jeśli wszyscy zaczną polegać na ADL, wszystkie niestandardowe kontenery mają aby zaimplementować dodatkowe funkcje begin () / end () w wymaganych przestrzeniach nazw. Jednak niektóre źródła wydają się sugerować, że niewykwalifikowane wywołania do rozpoczęcia/zakończenia są zalecanym sposobem (np. https://svn.boost.org/trac/boost/ticket/6357).

Więc co to jest sposób C++11? Czy autorzy bibliotek kontenerów powinni pisać dodatkowe funkcje begin/end dla swoich klas, aby obsługiwać niekwalifikowane wywołania begin / end w przypadku braku użycia przestrzeni nazw std; lub użycia STD::begin;?

Author: TemplateRex, 2013-07-10

1 answers

Istnieje kilka podejść, każdy z własnymi zaletami i wadami. Poniżej trzy podejścia z analizą kosztów i korzyści.

ADL przez Niestandardowy nieczłonek begin() / end()

Pierwsza alternatywa dostarcza szablony funkcji spoza begin() i end() wewnątrz przestrzeni nazw, aby zmodernizować wymaganą funkcjonalność do dowolnej klasy lub szablonu klasy, który może ją dostarczyć, ale ma np. złą konwencję nazewnictwa. Kod wywołujący może wtedy polegać na ADL, aby znaleźć te nowe funkcje. Przykładowy kod (na podstawie komentarzy @Xeo):

// LegacyContainerBeginEnd.h
namespace legacy {

// retro-fitting begin() / end() interface on legacy 
// Container class template with incompatible names         
template<class C> 
auto begin(Container& c) -> decltype(c.legacy_begin())
{ 
    return c.legacy_begin(); 
}

// similarly for begin() taking const&, cbegin(), end(), cend(), etc.

} // namespace legacy

// print.h
template<class C>
void print(C const& c)
{
    // bring into scope to fall back on for types without their own namespace non-member begin()/end()
    using std::begin;
    using std::end;

    // works for Standard Containers, C-style arrays and legacy Containers
    std::copy(begin(c), end(c), std::ostream_iterator<decltype(*begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}

Plusy: w 2007 roku, po raz pierwszy w Polsce, w Polsce i za granicą, w 2009 roku, w Polsce i za granicą.]}

  • działa dla wszystkich standardowych kontenerów i typów użytkowników, które definiują member .begin() i .end()
  • działa dla tablic w stylu C
  • można doposażyć do pracy (również dla range-dla pętli!) dla dowolnego szablonu klasy legacy::Container<T> który nie ma członu .begin() i end() bez konieczności podawania źródła modyfikacje kodu

Cons : wymaga użycia deklaracji w wielu miejscach

  • std::begin i std::end muszą być wprowadzone do każdego jawnego zakresu wywołania jako opcje awaryjne dla tablic w stylu C (potencjalne pułapki dla nagłówków szablonów i ogólne uciążliwości)

ADL przez niestandardowe Nie-członka adl_begin() i adl_end()

W przeciwieństwie do innych rozwiązań, nie można ich używać w systemach Windows, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X, Mac OS X]} przestrzeń nazw poprzez dostarczenie nieczłonkowych szablonów funkcji adl_begin() i adl_end(), które następnie można znaleźć również poprzez ADL. Przykładowy kod (na podstawie komentarzy @Yakk):

// LegacyContainerBeginEnd.h 
// as before...

// ADLBeginEnd.h
namespace adl {

using std::begin; // <-- here, because otherwise decltype() will not find it 

template<class C> 
auto adl_begin(C && c) -> decltype(begin(std::forward<C>(c)))
{ 
    // using std::begin; // in C++14 this might work because decltype() is no longer needed
    return begin(std::forward<C>(c)); // try to find non-member, fall back on std::
}

// similary for cbegin(), end(), cend(), etc.

} // namespace adl

using adl::adl_begin; // will be visible in any compilation unit that includes this header

// print.h
# include "ADLBeginEnd.h" // brings adl_begin() and adl_end() into scope

template<class C>
void print(C const& c)
{
    // works for Standard Containers, C-style arrays and legacy Containers
    std::copy(adl_begin(c), adl_end(c), std::ostream_iterator<decltype(*adl_begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and legacy Containers
    // does not need adl_begin() / adl_end(), but continues to work
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}

Plusy : spójna konwencja wywołująca, która działa całkowicie generycznie

  • te same plusy co za sugestia @Xeo +
  • powtarzające się deklaracje użycia zostały zamknięte (suche)

Wady : trochę gadatliwy

  • adl_begin() / adl_end() nie jest jak terse as begin() / end()
  • Nie jest też tak samo idiomatyczny (choć jest jednoznaczny).]}
  • W oczekiwaniu na dedukcję typu zwracanego przez C++14, będzie również zanieczyszczać przestrzeń nazw za pomocą std::begin / std::end

Uwaga: Nie jestem pewien, czy to naprawdę poprawia się w stosunku do poprzedniego podejścia.

std::begin() lubstd::end() wszędzie

Raz werbalność begin() / end() została poddana i tak, dlaczego nie wrócić do kwalifikowanych połączeń z std::begin() / std::end()? Przykładowy kod:

// LegacyIntContainerBeginEnd.h
namespace std {

// retro-fitting begin() / end() interface on legacy IntContainer class 
// with incompatible names         
template<> 
auto begin(legacy::IntContainer& c) -> decltype(c.legacy_begin())
{ 
    return c.legacy_begin(); 
}

// similary for begin() taking const&, cbegin(), end(), cend(), etc.

} // namespace std

// LegacyContainer.h
namespace legacy {

template<class T>
class Container
{
public:
    // YES, DOCUMENT REALLY WELL THAT THE EXISTING CODE IS BEING MODIFIED
    auto begin() -> decltype(legacy_begin()) { return legacy_begin(); }
    auto end() -> decltype(legacy_end()) { return legacy_end(); }

    // rest of existing interface
};

} // namespace legacy

// print.h
template<class C>
void print(C const& c)
{
    // works for Standard Containers, C-style arrays as well as 
    // legacy::IntContainer and legacy::Container<T>
    std::copy(std::begin(c), std::end(c), std::ostream_iterator<decltype(*std::begin(c))>(std::cout, " ")); std::cout << "\n";

    // alternative: also works for Standard Containers, C-style arrays and
    // legacy::IntContainer and legacy::Container<T>
    for (auto elem: c) std::cout << elem << " "; std::cout << "\n";
}

Plusy : spójna konwencja wywołująca, która działa prawie generycznie

  • działa dla wszystkich standardowych kontenerów i typów użytkowników, które definiują member .begin() i .end()
  • działa dla tablic w stylu C

W 2007 roku, w ramach programu "Horyzont 2020", w ramach programu "Horyzont 2020", w ramach programu "Horyzont 2020" - program ramowy]}

  • std::begin() / std::end() jest nieco bardziej wyrazisty niż begin() / end()
  • można doposażyć tylko do pracy (również for range-for loops !) dla dowolnej klasy LegacyContainer który nie ma member .begin() i end() (i dla którego nie ma kodu źródłowego!) poprzez podanie jawnych specjalizacji nieczłonkowych szablonów funkcji begin() i end() w namespace std
  • można doposażyć tylko w szablony klas LegacyContainer<T> poprzez bezpośrednie dodawanie funkcji Członkowskich begin() / end() wewnątrz kodu źródłowego LegacyContainer<T> (który dla szablonów jest dostępny). namespace std sztuczka nie działa tutaj, ponieważ szablony funkcji nie mogą być częściowo wyspecjalizowane. 

Czego używać?

Podejście ADL przez osoby niebędące członkami begin() / end() w kontenerze a przestrzeń nazw jest idiomatyczne podejście C++11, szczególnie dla funkcji generycznych, które wymagają modernizacji na starszych klasach i szablonach klas. Jest to ten sam idiom, co dla funkcji nieczłonkowych swap() udostępniających użytkownika.

Dla kodu, który używa tylko standardowych kontenerów lub tablic w stylu C, std::begin() i std::end() można nazwać wszędzie bez wprowadzania using-declarations, kosztem bardziej gadatliwych wywołań. To podejście może być nawet zmodernizowane, ale wymaga modyfikowania namespace std (dla typów klas) lub modyfikowania źródeł (dla szablonów klas). Można to zrobić, ale nie jest warte kłopotów z utrzymaniem.

W kodzie niestandardowym, gdzie dany kontener jest znany w czasie kodowania, można nawet polegać na ADL tylko dla standardowych kontenerów i wyraźnie kwalifikować std::begin / std::end dla C-style tablice. Traci pewną spójność wywołania, ale oszczędza na używaniu deklaracji.

 34
Author: TemplateRex,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-11 07:57:24