Encapsuleur Const pour l'itérateur

Oct 11 2020

Edit : ajout de précisions sur les raisons pour lesquelles je veux cela et mise à jour du code car je n'ai pas encore de réponses

J'ai une classe de type tableau C ++ 11 qui (peut être) un wrapper autour d'un itérateur à accès aléatoire. L'accès basé sur un index et .begin()/ .end()peut simplement passer par l'itérateur, mais il y a un peu de complication lorsque l'objet est 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();}
    };

Si nous revenions simplement DataIteratorde la constversion de .begin()and .end(), alors les personnes détenant un Storage const &seraient (à tort!) Capables de modifier le tableau via cet itérateur.

Les conteneurs comme std::vectoront deux itérateurs séparés ( ::iteratoret ::const_iterator), mais notre Storageclasse n'a que celui en lecture-écriture, donc nous en synthétisons un en utilisant 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'implémentation transfère toutes les méthodes pertinentes et dispose d'une spécialisation pour ConstWrapper<ConstWrapper<...>>qu'elle ne puisse pas s'envelopper.

Un élément clé est MakeConst, qui transforme les types en la bonne constvariante (contrairement au simple ajout const, qui n'a aucun effet sur les références):

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

Est-ce que cela a du sens et est-ce que ça va? Y a-t-il autre chose que je pourrais faire qui soit plus lisible / efficace / etc.?

Merci!

Réponses

2 Quuxplusone Oct 25 2020 at 06:30

Je ne pense pas que votre spécialisation fasse ce que vous pensez qu'elle fait. https://godbolt.org/z/Gfh7sv

Vous ne voulez ConstWrapper<ConstWrapper<Iterator>>pas hériter de ConstWrapper<Iterator> ; cela signifierait qu'un CW<CW<I>>"est-une-sorte-de" CW<I>, ce qui n'est pas vrai. Vous ne voulez pas d'une relation d'héritage ici. Ce que vous voulez est simplement pour les spécialisations de Storagede ne pas envelopper DataIterator s qui se trouvent spécialisations ConstWrapperdéjà. La façon dont vous faites cela est avec une couche supplémentaire d'indirection d'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;
    ~~~
};

Maintenant, si DataIteratorest déjà un ConstWrapper, alors les deux iteratoret const_iteratorseront littéralement du même type ... ce que vous voulez.


Votre MakeConstsemble être fondé sur l'idée douteuse qui const _Bit_referencene sera pas assignable. Cela changera probablement dans C ++ 23. Voirhttps://stackoverflow.com/questions/63412623/should-c20-stdrangessort-not-need-to-support-stdvectorbool pour un certain contexte.


Vous répétez une certaine métaprogrammation dans les types de retour de operator*et operator[]. Ils doivent simplement renvoyer le type referenceque vous avez déjà calculé ci-dessus.

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

Incidemment, je change le nom de votre membre de données de iteratoren it_, car je soupçonne que c'est une idée terrible, du point de vue des concepts, d'avoir un type de classe conteneur-ish avec un membre nommé iteratorquel membre est (A) non public, et (B ) pas un type.


Vous operator!=devriez probablement utiliser l'idiome ami caché:

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

En C ++ 20, vous pourriez théoriquement omettre operator!=et fournir operator==uniquement. Je ne sais pas encore si c'est une bonne idée.

Passer ConstWrapperpar valeur devrait être bien, car il ne contient qu'un itérateur et les itérateurs sont peu coûteux à copier.