Opakowanie Const dla iteratora

Oct 11 2020

Edycja : dodano wyjaśnienie, dlaczego tego chcę, i zaktualizowałem kod, ponieważ nie mam jeszcze żadnych odpowiedzi

Mam klasę podobną do tablicy w C ++ 11, która (może być) otoką wokół iteratora o swobodnym dostępie. Dostęp oparty na indeksach i .begin()/ .end()może po prostu przejść do iteratora, ale jest trochę komplikacji, gdy obiekt jest const:

    template<class Size, typename DataIterator>
    class Storage : public Size {
        DataIterator iterator;
    public:
        Storage(const DataIterator &iterator, const Size &size) : Size(size), iterator(iterator) {}
        
        auto operator[](index_t i)
                -> decltype(iterator[i]) {
            return iterator[i];
        }
        auto operator[](index_t i) const
                -> MakeConst<decltype(iterator[i])> {
            return iterator[i];
        }
        
        DataIterator begin() {return iterator;}
        ConstWrapper<DataIterator> begin() const {return iterator;}
        DataIterator end() {return iterator + this->size();}
        ConstWrapper<DataIterator> end() const {return iterator + this->size();}
    };

Gdybyśmy po prostu wrócili DataIteratorz constwersji .begin()i .end(), to osoby posiadające Storage const &(niepoprawnie!) Mogłyby zmodyfikować tablicę za pomocą tego iteratora.

Kontenery takie jak std::vectormają dwa oddzielne iteratory ( ::iteratori ::const_iterator), ale nasza Storageklasa ma tylko jeden do odczytu i zapisu, więc syntetyzujemy jeden za pomocą ConstWrapper:

    template <typename Iterator>
    class ConstWrapper {
        Iterator iterator;

        using traits = std::iterator_traits<Iterator>;
    public:
        using difference_type = typename traits::difference_type;
        using value_type = typename traits::value_type;
        using pointer = ConstWrapper;
        using reference = MakeConst<typename traits::reference>;
        using iterator_category = typename traits::iterator_category;
    
        ConstWrapper() {}
        ConstWrapper(const Iterator &iterator) : iterator(iterator) {}
        
        // The problematic cases:
        auto operator[](index_t i) const
                -> MakeConst<decltype(iterator[i])> {
            return iterator[i];
        }
        auto operator*() const
                -> MakeConst<decltype(*iterator)> {
            return *iterator;
        }

        bool operator!= (const ConstWrapper& other) const {
            return iterator != other.iterator;
        }
        /** All the other random-access iterator methods **/
    };
    
    // Specialisation to prevent infinite loops
    template <typename Iterator>
    class ConstWrapper<ConstWrapper<Iterator>> : ConstWrapper<Iterator> {
    public:
        using ConstWrapper<Iterator>::ConstWrapper;
    };

Implementacja przekazuje wszystkie odpowiednie metody i ma specjalizację ConstWrapper<ConstWrapper<...>>, aby nie mogła się zawinąć.

Kluczową częścią jest to MakeConst, że zmienia typy we właściwy constwariant (w przeciwieństwie do prostego dodawania const, które nie ma wpływu na odwołania):

    // Converts (T & -> T const &), and (T -> const T)
    using MakeConst = typename std::conditional<
        std::is_reference<T>::value,
        typename std::remove_reference<T>::type const &,
        const T
    >::type;

Czy to ma sens i czy jest w porządku? Czy jest coś, co mógłbym zrobić, co jest bardziej czytelne / wydajne / itp.?

Dzięki!

Odpowiedzi

2 Quuxplusone Oct 25 2020 at 06:30

Nie sądzę, że twoja specjalizacja polega na robieniu tego, co myślisz, że robi. https://godbolt.org/z/Gfh7sv

Nie chcesz ConstWrapper<ConstWrapper<Iterator>>, aby dziedziczyć z ConstWrapper<Iterator> ; oznaczałoby to, że coś CW<CW<I>>jest w rodzaju CW<I>, co nie jest prawdą. Nie chcesz tutaj związku spadkowego. Co chcesz to po prostu dla specjalizacji Storageaby nie zawijać DataIterator s, które zdarzają się specjalizacje ConstWrapperjuż. Sposób, w jaki to robisz, polega na dodatkowej warstwie aliasów pośrednich:

template<class T> struct maybe_constwrap { using type = ConstWrapper<T>; };
template<class U> struct maybe_constwrap<ConstWrapper<U>> { using type = ConstWrapper<U>; };

template<class Size, typename DataIterator>
class Storage : public Size {
public:
    using iterator = DataIterator;
    using const_iterator = typename maybe_constwrap<DataIterator>::type;
    ~~~
};

Teraz, jeśli DataIteratorjest już a ConstWrapper, to oba iteratori const_iteratorbędą dosłownie tego samego typu ... a właśnie tego chcesz.


Twój MakeConstwydaje się być oparty na wątpliwej idei, której const _Bit_referencenie można przypisać. To się prawdopodobnie zmieni w C ++ 23. Widziećhttps://stackoverflow.com/questions/63412623/should-c20-stdrangessort-not-need-to-support-stdvectorbool w jakimś kontekście.


Powtarzasz pewne metaprogramowanie w zwracanych typach operator*i operator[]. Powinny po prostu zwrócić typ, referencektóry już obliczyłeś powyżej.

reference operator*() const { return *it_; }

Nawiasem mówiąc, zmieniam nazwę członka danych z iteratorna it_, ponieważ podejrzewam, że to okropny pomysł, jeśli chodzi o koncepcje, aby mieć typ klasy kontenera z członkiem o nazwie, iteratorktóry członek jest (A) niepubliczny, i (B ) nie jest typem.


Twój operator!=prawdopodobnie powinien użyć ukrytego przyjaciela idiom:

friend bool operator==(ConstWrapper a, ConstWrapper b) { return a.it_ == b.it_; }
friend bool operator!=(ConstWrapper a, ConstWrapper b) { return a.it_ != b.it_; }

W C ++ 20 teoretycznie można by było pominąć operator!=i podać operator==tylko. Nie wiem jeszcze, czy to dobry pomysł.

Przekazywanie ConstWrapperwartości powinno być w porządku, ponieważ zawiera tylko iterator, a iteratory są tanie w kopiowaniu.