Search code examples
c++c++11iteratorconstantsshared-ptr

C++11 cast const iterator pointing to container of shared_ptr objects


I have an STL container whose element type is const std::shared_ptr<MyClass>.

I want to supply two iterator types to the user:

  1. MyContainer::iterator

typedefed as std::vector<const std::shared_ptr<MyClass>>::iterator (which should be the same type as std::vector<const std::shared_ptr<const MyClass>>::const_iterator

  1. MyContainer::const_iterator

typedefed as std::vector<const std::shared_ptr<const MyClass>>::iterator (which should be the same type as std::vector<const std::shared_ptr<const MyClass>>::const_iterator

In other words, I want the "const" to refer to the MyClass constness, not shared_ptr constness. The solution I found for getting the second iterator type is getting the first one, which is easy (e.g. using vector::begin), and then converting it to the second type using static_cast (fixme: no need to use const_cast because I'm adding constness, not removing it).

Would that be the common good-design way to achieve that, or there's a better/more common way?


Solution

  • typedefed as std::vector<const std::shared_ptr<MyClass>>::iterator (which should be the same type as std::vector<std::shared_ptr<const MyClass>>::const_iterator

    But it probably isn't the same type. Iterators are not just pointers. If the iterator and const_iterator types are defined inside vector then they are completely unrelated types:

    template<typename T>
    class vector
    {
        class iterator;
        class const_iterator;
        // ...
    

    vector<const int> is a different type to vector<int> and so their nested types are also different. As far as the compiler is concerned they are completely unrelated types, i.e. you cannot just move const around to any point in this type and get compatible types:

    vector<const shared_ptr<const T>>::iterator
    

    You cannot use const_cast to convert between unrelated types. You can use static_cast to convert a vector<T>::iterator to a vector<T>::const_iterator but it's not really a cast, you're constructing the latter from the former, which is allowed because that conversion is required by the standard.

    You can convert a shared_ptr<const T> to a shared_ptr<T> with const_pointer_cast<T> but again only because it's defined to work by the standard, not because the types are inherently compatible and not because it "just works" like plain ol' pointers.

    Since vector's iterators don't provide the deep-constness you want, you'll need to write your own, but it's not hard:

    class MyClass { };
    
    class MyContainer
    {
        typedef std::vector<std::shared_ptr<MyClass>> container_type;
    
        container_type m_cont;
    
    public:
    
        typedef container_type::iterator iterator;
    
        class const_iterator
        {
            typedef container_type::const_iterator internal_iterator;
            typedef std::iterator_traits<internal_iterator> internal_traits;
    
            const_iterator(internal_iterator i) : m_internal(i) { }
            friend class MyContainer;
    
        public:
    
            const_iterator() { }
            const_iterator(iterator i) : m_internal(i) { }
    
            typedef std::shared_ptr<const MyClass> value_type;
            typedef const value_type& reference;
            typedef const value_type* pointer;
            typedef internal_traits::difference_type difference_type;
            typedef internal_traits::iterator_category iterator_category;
    
            const_iterator& operator++() { ++m_internal; return *this; }
            const_iterator operator++(int) { const_iterator tmp = *this; ++m_internal; return tmp; }
    
            reference operator*() const { m_value = *m_internal; return m_value; }
            pointer operator->() const { m_value = *m_internal; return &m_value; }
    
            // ...
    
        private:
            internal_iterator m_internal;
            mutable value_type m_value;
        };
    
        iterator begin() { return m_cont.begin(); }
        const_iterator begin() const { return const_iterator(m_cont.begin()); }
    
        // ...    
    };
    

    That iterator type is mising a few things (operator--, operator+) but they're easy to add, following the same ideas as already shown.

    The key point to notice is that in order for const_iterator::operator* to return a reference, there needs to be a shared_ptr<const MyClass> object stored as a member of the iterator. That member acts as a "cache" for the shared_ptr<const MyClass> value, because the underlying container's real elements are a different type, shared_ptr<MyClass>, so you need somewhere to cache the converted value so a reference to it can be returned. N.B. Doing this slightly breaks the iterator requirements, because the following doesn't work as expected:

    MyContainer::const_iterator ci = c.begin();
    const shared_ptr<const MyClass>& ref = *ci;
    const MyClass* ptr = ref.get();
    ++ci;
    (void) *ci;
    assert( ptr == ref.get() );  // FAIL!
    

    The reason the assertion fails is that *ci doesn't return a reference to an underlying element of the container, but to a member of the iterator, which gets modified by the following increment and dereference. If this behaviour isn't acceptable you'll need to return a proxy from your iterator instead of caching a value. Or return a shared_ptr<const MyClass> when the const_iterator is dereferenced. (The difficulties of getting this 100% right is one of the reasons STL containers don't try to model deep constness!)

    A lot of the effort of defining your own iterator types is done for you by the boost::iterator_adaptor utility, so the example above is only really useful for exposition. With that adaptor you'd only need to do this to get your own custom iterator types with the desired behaviour:

    struct iterator
    : boost::iterator_adaptor<iterator, container_type::iterator>
    {
        iterator() { }
        iterator(container_type::iterator i) : iterator_adaptor(i) { }
    };
    
    struct const_iterator
    : boost::iterator_adaptor<const_iterator, container_type::const_iterator, std::shared_ptr<const MyClass>, boost::use_default, std::shared_ptr<const MyClass>>
    {
        const_iterator() { }
        const_iterator(iterator i) : iterator_adaptor(i.base()) { }
        const_iterator(container_type::const_iterator i) : iterator_adaptor(i) { }
    };