Const-Wrapper für Iterator

Oct 11 2020

Bearbeiten : Klarstellung hinzugefügt, warum ich das möchte, und Code aktualisiert, da ich noch keine Antworten habe

Ich habe eine Array-ähnliche C ++ 11-Klasse, die ein Wrapper um einen Iterator mit wahlfreiem Zugriff sein kann. Indexbasierter Zugriff und .begin()/ .end()oder Übergabe an den Iterator, aber es gibt einige Komplikationen, wenn das Objekt 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();}
    };

Wenn wir einfach DataIteratorvon der constVersion von .begin()und zurückkehren würden, könnten .end()Personen, die a halten Storage const &(falsch!), Das Array über diesen Iterator ändern.

Container wie std::vectorhaben zwei separate Iteratoren ( ::iteratorund ::const_iterator), aber unsere StorageKlasse hat nur den Lese- / Schreib- Iterator , also synthetisieren wir einen mit 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;
    };

Die Implementierung leitet alle relevanten Methoden weiter und ist darauf spezialisiert ConstWrapper<ConstWrapper<...>>, dass sie sich nicht selbst umschließen kann.

Ein wichtiger Teil ist MakeConst, der Typen in die richtige constVariante verwandelt (im Gegensatz zum einfachen Hinzufügen const, das keine Auswirkungen auf Referenzen hat):

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

Ist das sinnvoll und in Ordnung? Gibt es noch etwas, das ich tun könnte, das lesbarer / effizienter / etc. Ist?

Vielen Dank!

Antworten

2 Quuxplusone Oct 25 2020 at 06:30

Ich glaube nicht, dass Ihre Spezialisierung das tut, was Sie denken. https://godbolt.org/z/Gfh7sv

Sie wollen nicht ConstWrapper<ConstWrapper<Iterator>>zu vererben ConstWrapper<Iterator> ; das würde bedeuten, dass ein CW<CW<I>>"ist eine Art" CW<I>, was nicht wahr ist. Sie möchten hier keine Vererbungsbeziehung. Was Sie wollen, ist einfach für Spezialisierungen Storage, um DataIterator s nicht zu verpacken , die zufällig Spezialisierungen von ConstWrapperbereits sind. Die Art und Weise, wie Sie dies tun, ist mit einer zusätzlichen Ebene der Alias-Indirektion:

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

Nun, wenn DataIteratores bereits ein ist ConstWrapper, dann sind beide iteratorund const_iteratorbuchstäblich der gleiche Typ ... was Sie wollen.


Ihr MakeConstscheint auf der zweifelhaften Idee const _Bit_referencezu beruhen, der nicht zugeordnet werden kann. Das wird sich in C ++ 23 wahrscheinlich ändern. Sehenhttps://stackoverflow.com/questions/63412623/should-c20-stdrangessort-not-need-to-support-stdvectorbool für einen bestimmten Kontext.


Sie wiederholen einige Metaprogrammierungen in den Rückgabetypen von operator*und operator[]. Sie sollten nur den Typ zurückgeben, referenceden Sie oben bereits berechnet haben.

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

Im Übrigen ändere ich den Namen Ihres Datenmitglieds von in iteratorin it_, da ich vermute, dass es konzeptionell eine schreckliche Idee ist, einen container-artigen Klassentyp mit einem Mitglied zu haben, iteratordessen Mitglied (A) nicht öffentlich ist, und (B. ) kein Typ.


Sie operator!=sollten wahrscheinlich die Sprache der versteckten Freunde verwenden:

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

In C ++ 20 könnten Sie theoretisch nur weglassen operator!=und bereitstellen operator==. Ich weiß noch nicht, ob das eine gute Idee ist.

Das Übergeben ConstWrappervon Werten sollte in Ordnung sein, da es nur einen Iterator enthält und Iteratoren billig zu kopieren sind.