Jak wdrożyć iterator w stylu STL i uniknąć typowych pułapek?

306

Zrobiłem kolekcję, dla której chcę zapewnić iterator o swobodnym dostępie w stylu STL. Szukałem przykładowej implementacji iteratora, ale nie znalazłem żadnej. Wiem o potrzebie przeciążeń stałych []i *operatorów. Jakie są wymagania, aby iterator był „w stylu STL” i jakich innych pułapek należy unikać (jeśli w ogóle)?

Dodatkowy kontekst: dotyczy biblioteki i nie chcę od niej zależeć, chyba że naprawdę muszę. Piszę własną kolekcję, aby zapewnić zgodność binarną między C ++ 03 i C ++ 11 z tym samym kompilatorem (więc nie ma STL, który prawdopodobnie by się zepsuł).

Tamás Szelei
źródło
13
+1! Fajne pytanie. Zastanawiałem się nad tym samym. Łatwo jest przesuwać coś razem w oparciu o Boost.Iterator, ale zaskakująco trudno jest po prostu znaleźć listę wymagań, jeśli wdrożysz to od zera.
lipiec
2
Pamiętaj też, że twoje iteratory muszą być przerażające. boost.org/doc/libs/1_55_0/doc/html/intrusive/…
alfC

Odpowiedzi:

232

http://www.cplusplus.com/reference/std/iterator/ ma poręczną tabelę, która szczegółowo opisuje specyfikację § 24.2.2 standardu C ++ 11. Zasadniczo iteratory mają znaczniki opisujące prawidłowe operacje, a znaczniki mają hierarchię. Poniżej jest czysto symboliczny, te klasy tak naprawdę nie istnieją jako takie.

iterator {
    iterator(const iterator&);
    ~iterator();
    iterator& operator=(const iterator&);
    iterator& operator++(); //prefix increment
    reference operator*() const;
    friend void swap(iterator& lhs, iterator& rhs); //C++11 I think
};

input_iterator : public virtual iterator {
    iterator operator++(int); //postfix increment
    value_type operator*() const;
    pointer operator->() const;
    friend bool operator==(const iterator&, const iterator&);
    friend bool operator!=(const iterator&, const iterator&); 
};
//once an input iterator has been dereferenced, it is 
//undefined to dereference one before that.

output_iterator : public virtual iterator {
    reference operator*() const;
    iterator operator++(int); //postfix increment
};
//dereferences may only be on the left side of an assignment
//once an output iterator has been dereferenced, it is 
//undefined to dereference one before that.

forward_iterator : input_iterator, output_iterator {
    forward_iterator();
};
//multiple passes allowed

bidirectional_iterator : forward_iterator {
    iterator& operator--(); //prefix decrement
    iterator operator--(int); //postfix decrement
};

random_access_iterator : bidirectional_iterator {
    friend bool operator<(const iterator&, const iterator&);
    friend bool operator>(const iterator&, const iterator&);
    friend bool operator<=(const iterator&, const iterator&);
    friend bool operator>=(const iterator&, const iterator&);

    iterator& operator+=(size_type);
    friend iterator operator+(const iterator&, size_type);
    friend iterator operator+(size_type, const iterator&);
    iterator& operator-=(size_type);  
    friend iterator operator-(const iterator&, size_type);
    friend difference_type operator-(iterator, iterator);

    reference operator[](size_type) const;
};

contiguous_iterator : random_access_iterator { //C++17
}; //elements are stored contiguously in memory.

Możesz albo specjalizować się std::iterator_traits<youriterator>, albo wstawiać te same typy typedef w samym iteratorze lub dziedziczyć po std::iterator(który ma te typy typedef). Wolę drugą opcję, aby uniknąć zmiany rzeczy w stdprzestrzeni nazw i dla czytelności, ale większość ludzi dziedziczy std::iterator.

struct std::iterator_traits<youriterator> {        
    typedef ???? difference_type; //almost always ptrdiff_t
    typedef ???? value_type; //almost always T
    typedef ???? reference; //almost always T& or const T&
    typedef ???? pointer; //almost always T* or const T*
    typedef ???? iterator_category;  //usually std::forward_iterator_tag or similar
};

Zauważ, że iterator_category powinno być jednym z std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tag, lub std::random_access_iterator_tag, w zależności od wymagań swoich iterator spełnia,. W zależności od iterator, można wybrać specjalizację std::next, std::prev, std::advance, a std::distancetakże, ale jest to rzadko potrzebne. W niezwykle rzadkich przypadkach możesz chcieć się specjalizować std::beginistd::end .

Twój kontener prawdopodobnie powinien mieć również const_iteratoriterator (ewentualnie zmienny) do stałych danych, który jest podobny do twojego, iteratorz wyjątkiem tego, że powinien być domyślnie możliwy do zbudowania z iteratora użytkownicy nie powinni mieć możliwości modyfikacji danych. Często wskaźnik wewnętrzny jest wskaźnikiem nietrwałych danych i iteratordziedziczy po nim const_iterator, aby zminimalizować powielanie kodu.

Mój post w Writing Your STL Container ma bardziej kompletny prototyp kontenera / iteratora.

Mucząca Kaczka
źródło
2
Oprócz specjalizacji std::iterator_traitslub samodzielnego definiowania typedefs, możesz także po prostu wyprowadzić się z std::iterator, co definiuje je dla Ciebie, w zależności od parametrów szablonu.
Christian Rau,
3
@LokiAstari: Pełna dokumentacja jest dość obszerna (40 stron w szkicu), a nie w zakresie przepełnienia stosu. Dodałem jednak więcej informacji wyszczególniających tagi iteratora i const_iterator. Czego jeszcze brakowało w moim poście? Wydaje się, że sugerujesz, że jest coś więcej do dodania do klasy, ale pytanie dotyczy konkretnie implementacji iteratorów.
Kaczka Mooing
5
std::iteratorzostał zaproponowany jako przestarzały w C ++ 17 ; tak nie było, ale nie liczyłbym na to, że będzie tu dużo dłużej.
einpoklum
2
Aktualizacja komentarza @ einpoklum: w końcu std::iteratorbyła przestarzała.
scry
1
@JathanathanLee: Wow, to operator booljest niesamowicie niebezpieczne. Ktoś spróbuje użyć tego do wykrycia końca zakresu while(it++), ale tak naprawdę sprawdza tylko, czy iterator został zbudowany z parametrem.
Mooing Duck,
16

Dokumentacja iterator_facade z Boost.Iterator zapewnia coś, co wygląda jak fajny samouczek na temat implementowania iteratorów dla połączonej listy. Czy możesz to wykorzystać jako punkt wyjścia do zbudowania iteratora o dostępie swobodnym nad kontenerem?

Jeśli nic więcej, możesz przyjrzeć się funkcjom członkowskim i typedefsom dostarczonym przez iterator_facadei użyć go jako punktu wyjścia do budowania własnych.

Michael Kristofik
źródło
10

Oto próbka surowego iteratora wskaźnika.

Nie powinieneś używać klasy iteratora do pracy z surowymi wskaźnikami!

#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <assert.h>

template<typename T>
class ptr_iterator
    : public std::iterator<std::forward_iterator_tag, T>
{
    typedef ptr_iterator<T>  iterator;
    pointer pos_;
public:
    ptr_iterator() : pos_(nullptr) {}
    ptr_iterator(T* v) : pos_(v) {}
    ~ptr_iterator() {}

    iterator  operator++(int) /* postfix */         { return pos_++; }
    iterator& operator++()    /* prefix */          { ++pos_; return *this; }
    reference operator* () const                    { return *pos_; }
    pointer   operator->() const                    { return pos_; }
    iterator  operator+ (difference_type v)   const { return pos_ + v; }
    bool      operator==(const iterator& rhs) const { return pos_ == rhs.pos_; }
    bool      operator!=(const iterator& rhs) const { return pos_ != rhs.pos_; }
};

template<typename T>
ptr_iterator<T> begin(T *val) { return ptr_iterator<T>(val); }


template<typename T, typename Tsize>
ptr_iterator<T> end(T *val, Tsize size) { return ptr_iterator<T>(val) + size; }

Surowe obejście oparte na zakresie wskaźnika. Proszę mnie poprawić, jeśli istnieje lepszy sposób na utworzenie pętli opartej na zakresie z surowego wskaźnika.

template<typename T>
class ptr_range
{
    T* begin_;
    T* end_;
public:
    ptr_range(T* ptr, size_t length) : begin_(ptr), end_(ptr + length) { assert(begin_ <= end_); }
    T* begin() const { return begin_; }
    T* end() const { return end_; }
};

template<typename T>
ptr_range<T> range(T* ptr, size_t length) { return ptr_range<T>(ptr, length); }

I prosty test

void DoIteratorTest()
{
    const static size_t size = 10;
    uint8_t *data = new uint8_t[size];
    {
        // Only for iterator test
        uint8_t n = '0';
        auto first = begin(data);
        auto last = end(data, size);
        for (auto it = first; it != last; ++it)
        {
            *it = n++;
        }

        // It's prefer to use the following way:
        for (const auto& n : range(data, size))
        {
            std::cout << " char: " << static_cast<char>(n) << std::endl;
        }
    }
    {
        // Only for iterator test
        ptr_iterator<uint8_t> first(data);
        ptr_iterator<uint8_t> last(first + size);
        std::vector<uint8_t> v1(first, last);

        // It's prefer to use the following way:
        std::vector<uint8_t> v2(data, data + size);
    }
    {
        std::list<std::vector<uint8_t>> queue_;
        queue_.emplace_back(begin(data), end(data, size));
        queue_.emplace_back(data, data + size);
    }
}
Valdemar_Rudolfovich
źródło
5

Przede wszystkim można zajrzeć tutaj , aby uzyskać listę różnych operacji poszczególne typy iteracyjnej potrzebują wsparcia.

Następnie, gdy utworzysz klasę iteratora, musisz albo się std::iterator_traitsw niej specjalizować i podać niezbędne typedefs (jak iterator_categorylub value_type), albo alternatywnie wyprowadzić ją z std::iterator, która określa typedefdla ciebie potrzebne s i dlatego może być używana z domyślną std::iterator_traits.

zrzeczenie się odpowiedzialności: Wiem, że niektórym ludziom się to nie podoba cplusplus.com, ale dostarczają naprawdę przydatnych informacji na ten temat.

Christian Rau
źródło
Naprawdę nie rozumiem sporu między cplusplus a cppreferencją, oba są dobre i brakuje wielu rzeczy. Jednak C ++ jest jedynym językiem, w którym implementacja standardowych iteratorów bibliotek to piekielny XD. Najczęściej pisanie klasy opakowania na kontenerze
STL
@GameDeveloper sprawdź bibliotekę szablonów, którą napisałem do implementacji iteratorów: github.com/VinGarcia/Simple-Iterator-Template . Jest to bardzo proste i wymaga tylko około 10 linii kodu do napisania iteratora.
VinGarcia,
Niezła klasa, doceniam to, prawdopodobnie warto go przenieść do kompilacji również z kontenerami innymi niż STL (EA_STL, UE4) .. Rozważ to! :)
CoffeDeveloper
W każdym razie, jeśli jedynym powodem jest to, że cplusplus.com dostarcza naprawdę przydatnych informacji, cppreference.com dostarcza więcej bardziej przydatnych informacji ...
LF
@LF Następnie możesz cofnąć się w czasie i dodać te informacje do wersji strony z 2011 r. ;-)
Christian Rau
3

Byłem / jestem na tej samej łodzi co ty z różnych powodów (częściowo edukacyjnych, częściowo ograniczeń). Musiałem przepisać wszystkie pojemniki ze standardowej biblioteki i pojemniki musiały być zgodne ze standardem. Oznacza to, że jeśli wymienię kontener z wersją stl , kod będzie działał tak samo. Co również oznaczało, że musiałem ponownie napisać iteratory.

W każdym razie spojrzałem na EASTL . Oprócz nauki tony o pojemnikach, których nigdy nie nauczyłem się za pomocą pojemników STL lub moich kursów licencjackich. Głównym powodem jest to, że EASTL jest bardziej czytelny niż odpowiednik stl (znalazłem to po prostu z powodu braku wszystkich makr i prostego stylu kodowania). Jest tam kilka obrzydliwych rzeczy (np. #Ifdefs za wyjątkami), ale nic cię nie przytłacza.

Jak wspomniano inni, spójrz na referencję cplusplus.com dotyczącą iteratorów i kontenerów.

Samaursa
źródło
3

Próbowałem rozwiązać problem z możliwością iteracji kilku różnych tablic tekstowych, z których wszystkie są przechowywane w dużej bazie danych rezydentnej pamięci struct.

Poniższe informacje opracowano przy użyciu Visual Studio 2017 Community Edition w aplikacji testowej MFC. Podaję to jako przykład, ponieważ ten post był jednym z kilku, które natknąłem się, które pod warunkiem, że pomoc była jeszcze niewystarczająca na moje potrzeby.

Te structzawierające dane rezydentami pamięci wyglądał mniej więcej tak. Usunąłem większość elementów ze względu na zwięzłość i nie uwzględniłem również używanych definicji Preprocesora (używany zestaw SDK jest przeznaczony zarówno dla C, jak i C ++ i jest stary).

Chciałem mieć iteratory dla różnych WCHARdwuwymiarowych tablic zawierających ciągi tekstowe dla mnemoników.

typedef struct  tagUNINTRAM {
    // stuff deleted ...
    WCHAR   ParaTransMnemo[MAX_TRANSM_NO][PARA_TRANSMNEMO_LEN]; /* prog #20 */
    WCHAR   ParaLeadThru[MAX_LEAD_NO][PARA_LEADTHRU_LEN];   /* prog #21 */
    WCHAR   ParaReportName[MAX_REPO_NO][PARA_REPORTNAME_LEN];   /* prog #22 */
    WCHAR   ParaSpeMnemo[MAX_SPEM_NO][PARA_SPEMNEMO_LEN];   /* prog #23 */
    WCHAR   ParaPCIF[MAX_PCIF_SIZE];            /* prog #39 */
    WCHAR   ParaAdjMnemo[MAX_ADJM_NO][PARA_ADJMNEMO_LEN];   /* prog #46 */
    WCHAR   ParaPrtModi[MAX_PRTMODI_NO][PARA_PRTMODI_LEN];  /* prog #47 */
    WCHAR   ParaMajorDEPT[MAX_MDEPT_NO][PARA_MAJORDEPT_LEN];    /* prog #48 */
    //  ... stuff deleted
} UNINIRAM;

Obecne podejście polega na użyciu szablonu do zdefiniowania klasy proxy dla każdej z tablic, a następnie posiadania pojedynczej klasy iteratora, której można użyć do iteracji po określonej tablicy za pomocą obiektu proxy reprezentującego tablicę.

Kopia danych rezydentnych pamięci jest przechowywana w obiekcie, który obsługuje odczytywanie i zapisywanie danych rezydentnych pamięci z / na dysk. Ta klasa,CFilePara przedstawiona na matrycy klasy proxy ( MnemonicIteratorDimSizei klasy sub, z którego ona pochodzi, MnemonicIteratorDimSizeBase) oraz klasę iteracyjnej, MnemonicIterator.

Utworzony obiekt proxy jest dołączony do obiektu iteratora, który uzyskuje dostęp do niezbędnych informacji poprzez interfejs opisany przez klasę podstawową, z której pochodzą wszystkie klasy proxy. W rezultacie powstaje jeden typ klasy iteratora, który może być używany z kilkoma różnymi klasami proxy, ponieważ wszystkie różne klasy proxy udostępniają ten sam interfejs, interfejs podstawowej klasy proxy.

Pierwszą rzeczą było stworzenie zestawu identyfikatorów, które byłyby dostarczane do fabryki klas w celu wygenerowania konkretnego obiektu proxy dla tego typu mnemonika. Te identyfikatory są używane jako część interfejsu użytkownika w celu identyfikacji konkretnych danych udostępniania, które użytkownik jest zainteresowany przeglądaniem i ewentualną modyfikacją.

const static DWORD_PTR dwId_TransactionMnemonic = 1;
const static DWORD_PTR dwId_ReportMnemonic = 2;
const static DWORD_PTR dwId_SpecialMnemonic = 3;
const static DWORD_PTR dwId_LeadThroughMnemonic = 4;

Klasa proxy

Templowana klasa proxy i jej klasa podstawowa są następujące. Musiałem pomieścić kilka różnych rodzajów wchar_ttablic tekstowych. Dwuwymiarowe tablice miały różną liczbę mnemoników, w zależności od typu (celu) mnemonika, a różne typy mnemoniki miały różne maksymalne długości, wahające się od pięciu znaków tekstowych do dwudziestu znaków tekstowych. Szablony dla pochodnej klasy proxy były w naturalny sposób dopasowane do szablonu wymagającego maksymalnej liczby znaków w każdym mnemoniku. Po utworzeniu obiektu proxy używamy SetRange()metody do określenia rzeczywistej tablicy mnemonicznej i jej zakresu.

// proxy object which represents a particular subsection of the
// memory resident database each of which is an array of wchar_t
// text arrays though the number of array elements may vary.
class MnemonicIteratorDimSizeBase
{
    DWORD_PTR  m_Type;

public:
    MnemonicIteratorDimSizeBase(DWORD_PTR x) { }
    virtual ~MnemonicIteratorDimSizeBase() { }

    virtual wchar_t *begin() = 0;
    virtual wchar_t *end() = 0;
    virtual wchar_t *get(int i) = 0;
    virtual int ItemSize() = 0;
    virtual int ItemCount() = 0;

    virtual DWORD_PTR ItemType() { return m_Type; }
};

template <size_t sDimSize>
class MnemonicIteratorDimSize : public MnemonicIteratorDimSizeBase
{
    wchar_t    (*m_begin)[sDimSize];
    wchar_t    (*m_end)[sDimSize];

public:
    MnemonicIteratorDimSize(DWORD_PTR x) : MnemonicIteratorDimSizeBase(x), m_begin(0), m_end(0) { }
    virtual ~MnemonicIteratorDimSize() { }

    virtual wchar_t *begin() { return m_begin[0]; }
    virtual wchar_t *end() { return m_end[0]; }
    virtual wchar_t *get(int i) { return m_begin[i]; }

    virtual int ItemSize() { return sDimSize; }
    virtual int ItemCount() { return m_end - m_begin; }

    void SetRange(wchar_t (*begin)[sDimSize], wchar_t (*end)[sDimSize]) {
        m_begin = begin; m_end = end;
    }

};

Klasa Iterator

Sama klasa iteratora wygląda następująco. Ta klasa zapewnia tylko podstawową funkcjonalność iteratora do przodu, która jest wszystkim, co jest potrzebne w tym momencie. Jednak oczekuję, że to się zmieni lub rozszerzy, gdy będę potrzebować czegoś dodatkowego.

class MnemonicIterator
{
private:
    MnemonicIteratorDimSizeBase   *m_p;  // we do not own this pointer. we just use it to access current item.
    int      m_index;                    // zero based index of item.
    wchar_t  *m_item;                    // value to be returned.

public:
    MnemonicIterator(MnemonicIteratorDimSizeBase *p) : m_p(p) { }
    ~MnemonicIterator() { }

    // a ranged for needs begin() and end() to determine the range.
    // the range is up to but not including what end() returns.
    MnemonicIterator & begin() { m_item = m_p->get(m_index = 0); return *this; }                 // begining of range of values for ranged for. first item
    MnemonicIterator & end() { m_item = m_p->get(m_index = m_p->ItemCount()); return *this; }    // end of range of values for ranged for. item after last item.
    MnemonicIterator & operator ++ () { m_item = m_p->get(++m_index); return *this; }            // prefix increment, ++p
    MnemonicIterator & operator ++ (int i) { m_item = m_p->get(m_index++); return *this; }       // postfix increment, p++
    bool operator != (MnemonicIterator &p) { return **this != *p; }                              // minimum logical operator is not equal to
    wchar_t * operator *() const { return m_item; }                                              // dereference iterator to get what is pointed to
};

Fabryka obiektów proxy określa, który obiekt ma zostać utworzony na podstawie identyfikatora mnemonicznego. Obiekt proxy jest tworzony, a zwracany wskaźnik jest standardowym typem klasy bazowej, aby mieć jednolity interfejs niezależnie od tego, które z różnych sekcji mnemonicznych są dostępne. SetRange()Sposób stosuje się w celu wskazania obiektu proxy poszczególnych elementów tablicy proxy reprezentuje oraz szereg elementów macierzy.

CFilePara::MnemonicIteratorDimSizeBase * CFilePara::MakeIterator(DWORD_PTR x)
{
    CFilePara::MnemonicIteratorDimSizeBase  *mi = nullptr;

    switch (x) {
    case dwId_TransactionMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_TRANSMNEMO_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_TRANSMNEMO_LEN>(x);
            mk->SetRange(&m_Para.ParaTransMnemo[0], &m_Para.ParaTransMnemo[MAX_TRANSM_NO]);
            mi = mk;
        }
        break;
    case dwId_ReportMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_REPORTNAME_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_REPORTNAME_LEN>(x);
            mk->SetRange(&m_Para.ParaReportName[0], &m_Para.ParaReportName[MAX_REPO_NO]);
            mi = mk;
        }
        break;
    case dwId_SpecialMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_SPEMNEMO_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_SPEMNEMO_LEN>(x);
            mk->SetRange(&m_Para.ParaSpeMnemo[0], &m_Para.ParaSpeMnemo[MAX_SPEM_NO]);
            mi = mk;
        }
        break;
    case dwId_LeadThroughMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_LEADTHRU_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_LEADTHRU_LEN>(x);
            mk->SetRange(&m_Para.ParaLeadThru[0], &m_Para.ParaLeadThru[MAX_LEAD_NO]);
            mi = mk;
        }
        break;
    }

    return mi;
}

Korzystanie z klasy proxy i iteratora

Klasa proxy i jej iterator są używane, jak pokazano w poniższej pętli, w celu wypełnienia CListCtrlobiektu listą mnemoników. Używam std::unique_ptrtak, że gdy klasa proxy nie jest już potrzebna, a std::unique_ptrpoza zasięgiem, pamięć zostanie wyczyszczona.

Tym kodem źródłowym jest utworzenie obiektu proxy dla tablicy w obrębie structodpowiadającego podanemu identyfikatorowi mnemonicznemu. Następnie tworzy iterator dla tego obiektu, używa zakresu fordo wypełnienia CListCtrlformantu, a następnie czyści. Są to nieprzetworzone wchar_tciągi tekstowe, które mogą być dokładnie liczbą elementów tablicy, więc kopiujemy ciąg do tymczasowego bufora, aby upewnić się, że tekst jest zerowany.

    std::unique_ptr<CFilePara::MnemonicIteratorDimSizeBase> pObj(pFile->MakeIterator(m_IteratorType));
    CFilePara::MnemonicIterator pIter(pObj.get());  // provide the raw pointer to the iterator who doesn't own it.

    int i = 0;    // CListCtrl index for zero based position to insert mnemonic.
    for (auto x : pIter)
    {
        WCHAR szText[32] = { 0 };     // Temporary buffer.

        wcsncpy_s(szText, 32, x, pObj->ItemSize());
        m_mnemonicList.InsertItem(i, szText);  i++;
    }
Richard Chambers
źródło
1

A teraz iterator kluczy dla pętli opartej na zakresie.

template<typename C>
class keys_it
{
    typename C::const_iterator it_;
public:
    using key_type        = typename C::key_type;
    using pointer         = typename C::key_type*;
    using difference_type = std::ptrdiff_t;

    keys_it(const typename C::const_iterator & it) : it_(it) {}

    keys_it         operator++(int               ) /* postfix */ { return it_++         ; }
    keys_it&        operator++(                  ) /*  prefix */ { ++it_; return *this  ; }
    const key_type& operator* (                  ) const         { return it_->first    ; }
    const key_type& operator->(                  ) const         { return it_->first    ; }
    keys_it         operator+ (difference_type v ) const         { return it_ + v       ; }
    bool            operator==(const keys_it& rhs) const         { return it_ == rhs.it_; }
    bool            operator!=(const keys_it& rhs) const         { return it_ != rhs.it_; }
};

template<typename C>
class keys_impl
{
    const C & c;
public:
    keys_impl(const C & container) : c(container) {}
    const keys_it<C> begin() const { return keys_it<C>(std::begin(c)); }
    const keys_it<C> end  () const { return keys_it<C>(std::end  (c)); }
};

template<typename C>
keys_impl<C> keys(const C & container) { return keys_impl<C>(container); }

Stosowanie:

std::map<std::string,int> my_map;
// fill my_map
for (const std::string & k : keys(my_map))
{
    // do things
}

Właśnie tego szukałem. Ale wydaje się, że nikt go nie miał.

Dostajesz moje wyrównanie kodu OCD jako bonus.

Jako ćwiczenie napisz własne values(my_map)

Gabriel
źródło