Const wrapper per iteratore

Oct 11 2020

Modifica : aggiunto il chiarimento sul motivo per cui lo voglio e aggiornato il codice poiché non ho ancora risposte

Ho una classe di tipo array C ++ 11 che (può essere) un wrapper attorno a un iteratore ad accesso casuale. L'accesso basato sull'indice e .begin()/ .end()può semplicemente passare all'iteratore, ma c'è un po 'di complicazioni quando l'oggetto è 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();}
    };

Se tornassimo semplicemente DataIteratordalla constversione di .begin()e .end(), le persone in possesso di un Storage const &(erroneamente!) Sarebbero in grado di modificare l'array tramite quell'iteratore.

Contenitori come std::vectorhanno due iteratori separati ( ::iteratore ::const_iterator), ma la nostra Storageclasse ha solo quello di lettura-scrittura, quindi ne sintetizziamo uno usando 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;
    };

L'implementazione inoltra tutti i metodi pertinenti e ha una specializzazione in ConstWrapper<ConstWrapper<...>>modo che non possa eseguire il wrapping.

Una parte fondamentale è MakeConstche trasforma i tipi nella constvariante corretta (a differenza della semplice aggiunta const, che non ha alcun effetto sui riferimenti):

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

Questo ha senso e va bene? C'è qualcos'altro che potrei fare che sia più leggibile / efficiente / ecc.?

Grazie!

Risposte

2 Quuxplusone Oct 25 2020 at 06:30

Non credo che la tua specializzazione stia facendo quello che pensi stia facendo. https://godbolt.org/z/Gfh7sv

Non si vuole ConstWrapper<ConstWrapper<Iterator>>a ereditare da ConstWrapper<Iterator> ; ciò significherebbe che un CW<CW<I>>"è-un-tipo-di" CW<I>, che non è vero. Non vuoi una relazione di eredità qui. Quello che vogliamo è semplicemente per specializzazioni di Storagea non avvolgere DataIterator s che capita di essere specializzazioni di ConstWrappergià. Il modo in cui lo fai è con un ulteriore livello di riferimento indiretto alias:

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

Ora, se DataIteratorè già a ConstWrapper, allora entrambi iteratore const_iteratorsaranno letteralmente dello stesso tipo ... che è quello che vuoi.


Il tuo MakeConstsembra basarsi sull'idea dubbia che const _Bit_referencenon sarà assegnabile a. È probabile che cambi in C ++ 23. Vederehttps://stackoverflow.com/questions/63412623/should-c20-stdrangessort-not-need-to-support-stdvectorbool per qualche contesto.


Ripeti alcune metaprogrammazioni nei tipi di ritorno di operator*e operator[]. Dovrebbero semplicemente restituire il tipo referenceche hai già calcolato sopra.

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

Per inciso, sto cambiando il nome del membro dei dati da iteratora it_, perché sospetto che sia una pessima idea, dal punto di vista dei concetti, avere un tipo di classe containerizzato con un membro denominato il iteratorcui membro è (A) non pubblico e (B ) non un tipo.


Il tuo operator!=dovrebbe probabilmente utilizzare il linguaggio nascosto amico:

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 potresti teoricamente omettere operator!=e fornire operator==solo. Non so ancora se sia una buona idea.

Il passaggio ConstWrapperper valore dovrebbe andare bene, perché contiene solo un iteratore e gli iteratori sono economici da copiare.