Search code examples
c++iteratorc++17const-correctnessconst-pointer

Using iterator to retrieve const values pointed to in containers


Const casting container value-types seems not possible. A comment in the other question suggests iterators as a solution, yet does not go into detail. Since I seemingly cannot simply convert a container from a non-const to a const version as a function parameter, I arrive at Iterators to maybe be able to do the job.

I actually have a vector<shared_ptr<Thing> > to be treated as const vector<shared_ptr<Thing const> >. With it I intend to use the shared_ptr<Thing const> as further references in other structures, without allowing those structures to alter the Things. Those structures may create their own objects, stored by their own shared_ptr, if they want slightly different content within their containers, while still actively sharing most Things with other objects.

So I would need either shared_ptr<const Thing>&, or const shared_ptr<const Thing>& from an Iterator through the sequence. Either would suffice, but just because one can be indifferent about passing references in this example, because of shared_ptr's copy semantics are about just that. Yet even just using default const_iterator, retrieved by cbegin(),c.end() and such, will give me a const shared_ptr<Thing>& instead.

Edit: To copy the vector element for element would be one way technically, as in the other question, yet undesired for interface reasons. I am going for reinterpretation here, not copy.

Any suggestions on where a workaround might lie?


Solution

  • Based on your situation, it sounds like defining a custom iterator with the semantics you want is the safe and simple way to go. It's technically correct, hard to accidentally misuse, and fairly fast, just requiring a shared_ptr copy on iterator dereference.

    I always recommend boost::iterator_facade or boost::iterator_adaptor for creating an iterator type. Since it will contain the original vector iterator as a "base" implementation, iterator_adaptor is helpful.

    class const_Thing_ptr_iterator :
        public boost::iterator_adaptor<
            const_Thing_ptr_iterator,                            // CRTP derived type
            std::vector<std::shared_ptr<Thing>>::const_iterator, // base iterator type
            std::shared_ptr<const Thing>,                        // value_type
            std::random_access_iterator_tag,                     // traversal type
            std::shared_ptr<const Thing>                         // reference
        >
    {
    public:
        const_Thing_ptr_iterator() = default;
        explicit const_Thing_ptr_iterator(base_type iter)
            : iterator_adaptor(iter) {}
    };
    

    The reference iterator type would be std::shared_ptr<const Thing>& by default, but in this case it can't be a reference since there is no object of that type. Normally the class would define some of the behavior functions like dereference or increment, but none are needed here: the only change from the base vector iterator is to the return type of operator*, and the default reference dereference() const { return *base_reference(); } works fine by implicit conversion from const std::shared_ptr<Thing>& to std::shared_ptr<const Thing>.

    The class could also be a template taking Thing as its type parameter, to create multiple iterator types.

    Then to provide a container-like view object, we can use C++20's std::ranges::subrange to provide begin() and end() and a few other things helping the out the range templates:

    #include <ranges>
    class const_Thing_ptrs_view
      : public std::ranges::subrange<const_Thing_ptr_iterator>
    {
    public:
        explicit const_Thing_ptrs_view(const std::vector<std::shared_ptr<Thing>> &vec)
            : subrange(const_Thing_ptr_iterator(vec.begin()),
                       const_Thing_ptr_iterator(vec.end())) {}
    };
    

    Or if that's not available, a simple class with begin() and end():

    class const_Thing_ptrs_view {
    public:
        explicit const_Thing_ptrs_view(const std::vector<std::shared_ptr<Thing>> &vec)
            : m_begin(vec.begin()), m_end(vec.end()) {}
        const_Thing_ptr_iterator begin() const { return m_begin; }
        const_Thing_ptr_iterator end() const { return m_end; }
    private:
        const_Thing_ptr_iterator m_begin;
        const_Thing_ptr_iterator m_end;
    };
    

    Demo on godbolt. (Clang doesn't like the ranges code due to this libstdc++ incompatibility; I'm not sure how to get godbolt to switch it to clang's libc++.)