Wrapper Const para iterador

Oct 11 2020

Editar : esclarecimento adicionado sobre por que eu quero isso e atualizei o código, pois ainda não tenho nenhuma resposta

Eu tenho uma classe de array-like C ++ 11 que (pode ser) um invólucro em torno de um iterador de acesso aleatório. Acesso baseado em índice e .begin()/ .end()pode simplesmente passar para o iterador, mas há um pouco de complicação quando o objeto é 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 simplesmente retornássemos DataIteratorda constversão de .begin()e .end(), então as pessoas segurando a Storage const &seriam (incorretamente!) Capazes de modificar o array por meio desse iterador.

Contêineres como std::vectortêm dois iteradores separados ( ::iteratore ::const_iterator), mas nossa Storageclasse só tem o de leitura e gravação, então sintetizamos um 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;
    };

A implementação encaminha todos os métodos relevantes e tem uma especialização para de ConstWrapper<ConstWrapper<...>>modo que não pode se embrulhar.

Uma parte importante é MakeConst, que transforma os tipos na constvariante correta (ao contrário de simplesmente adicionar const, que não tem efeito nas referências):

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

Isso faz sentido e está tudo bem? Há algo mais que eu poderia estar fazendo que seja mais legível / eficiente / etc.?

Obrigado!

Respostas

2 Quuxplusone Oct 25 2020 at 06:30

Não acho que sua especialização esteja fazendo o que você pensa que está fazendo. https://godbolt.org/z/Gfh7sv

Você não quer ConstWrapper<ConstWrapper<Iterator>>que herdam ConstWrapper<Iterator> ; isso significaria que um CW<CW<I>>"é-uma-espécie-de" CW<I>, o que não é verdade. Você não quer um relacionamento de herança aqui. O que você deseja é simplesmente que as especializações Storageou não envolvam DataIterator aquelas que ConstWrapperjá são especializações . A maneira de fazer isso é com uma camada extra de indireção de 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;
    ~~~
};

Agora, se DataIteratorjá for um ConstWrapper, então iteratore const_iteratorserão literalmente do mesmo tipo ... que é o que você deseja.


Você MakeConstparece se basear na idéia duvidosa de que const _Bit_referencenão será atribuível. É provável que isso mude no C ++ 23. Vejohttps://stackoverflow.com/questions/63412623/should-c20-stdrangessort-not-need-to-support-stdvectorbool para algum contexto.


Você repete alguma metaprogramação nos tipos de retorno de operator*e operator[]. Eles devem apenas retornar o tipo referenceque você já calculou acima.

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

A propósito, estou mudando o nome do seu membro de dados de iteratorpara it_, porque suspeito que seja uma ideia terrível, em termos de conceitos, ter um tipo de classe semelhante a um contêiner com um membro iteratorcujo membro é (A) não público e (B ) não um tipo.


Você operator!=provavelmente deve usar o idioma do amigo oculto:

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

Em C ++ 20, você poderia teoricamente omitir operator!=e fornecer operator==apenas. Não sei ainda se é uma boa ideia.

A passagem ConstWrapperpor valor deve ser boa, porque contém apenas um iterador, e iteradores são baratos para copiar.