2

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?

Sean
  • 9,888
  • 4
  • 40
  • 43

3 Answers3

2

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) { }
};
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • Then what should I do? I think what I need is very expected from a programmer to need, I mean, I want to have an iterator type which protects the pointed object. What do all the other users of containers-of-pointers in the world do? – cfa45ca55111016ee9269f0a52e771 Mar 01 '13 at 18:30
  • 1
    Define your own iterator types, instead of just returning `vector::iterator`, and make those types do the conversions from `shared_ptr` to `shared_ptr` as needed – Jonathan Wakely Mar 01 '13 at 18:32
  • How do I define my own iterator types? I don't have access to STL internals (and I don't want to have it either...) – cfa45ca55111016ee9269f0a52e771 Mar 01 '13 at 18:34
  • You don't need access to any internals to write an iterator, just write a type that models the iterator concept, wrapping a `vector>`, but returning a `const shared_ptr&` when the `const_iterator` type is dereferenced – Jonathan Wakely Mar 01 '13 at 18:36
  • 1
    I've added the start of a custom iterator implementation – Jonathan Wakely Mar 01 '13 at 18:49
  • 1
    Someone suggested I write a deep_shared_ptr class which wraps shared_ptr and const deep_shared_ptr does deep constness. Maybe this is better than a new iterator class? (I don't want the iterator to contain a copy of the object and I don't want proxy mess...) – cfa45ca55111016ee9269f0a52e771 Mar 01 '13 at 19:15
  • That could be simpler, yes. The `boost::iterator_adaptor` version works without a proxy or embedded shared_ptr member though (as @aschepler also suggests) – Jonathan Wakely Mar 01 '13 at 19:25
  • 1
    Even worse than the failure you noted is `const std::shared_ptr& ref = *c.begin();`. Instant dangling reference. And hmm, I guess we don't really need to implement `dereference()`, since the public `operator*` and so on will have correct types. – aschepler Mar 01 '13 at 20:27
  • How does boost::iterator_adaptor work? Why can't I get the same thing with plain C++ code? – cfa45ca55111016ee9269f0a52e771 Mar 03 '13 at 23:16
  • You can, there's no magic. The `iterator_adaptor` version returns by value, not reference, and takes care of all the boilerplate – Jonathan Wakely Mar 04 '13 at 00:18
2

boost::iterator_adaptor makes it pretty easy to define your own iterator types based on another iterator type. So you can set it up so that *iter is a const shared_ptr<MyClass>& or const shared_ptr<const MyClass>& as desired.

Though in the const_iterator case, dereferencing can't return a const shared_ptr<const MyClass>& if what you actually have is shared_ptr<MyClass>. So we'll define const_iterator::reference as just shared_ptr<const MyClass> and return by value.

#include <boost/iterator/iterator_adaptor.hpp>

class MyContainer {
public:

    class iterator;
    class const_iterator;

    class iterator :
        public boost::iterator_adaptor<
            iterator,                         // This class, for CRTP
            std::vector<const std::shared_ptr<MyClass>>::const_iterator,
                                              // Base type
            const std::shared_ptr<MyClass> >  // value_type
    {
    public:
        iterator() {}
        iterator(const iterator&) = default;

    private:
        friend class MyContainer;                 // allow private constructor
        friend class boost::iterator_core_access; // allow dereference()
        explicit iterator(base_type iter) : iterator_adaptor(iter) {}
        const std::shared_ptr<MyClass>& dereference() const
            { return *base_reference(); }
    };

    class const_iterator :
        public boost::iterator_adaptor<
            const_iterator,                        // This class, for CRTP
            std::vector<const std::shared_ptr<MyClass>>::const_iterator,
                                                   // Base type
            const std::shared_ptr<const MyClass>,  // value_type
            boost::use_default,                    // difference_type
            std::shared_ptr<const MyClass> >       // reference_type
    {
    public:
        const_iterator();
        const_iterator(const const_iterator&) = default;

        // Implicit conversion from iterator to const_iterator:
        const_iterator(const iterator& iter) : iterator_adaptor(iter.base()) {}

    private:
        friend class MyContainer;                 // allow private constructor
        friend class boost::iterator_core_access; // allow dereference()
        explicit const_iterator(base_type iter) : iterator_adaptor(iter) {}
        std::shared_ptr<const MyClass> dereference() const
            { return *base_reference(); }
    };

    iterator begin() { return iterator(mVec.begin()); }
    iterator end() { return iterator(mVec.end()); }
    const_iterator begin() const { return cbegin(); }
    const_iterator end() const { return cend(); }
    const_iterator cbegin() const { return const_iterator(mVec.begin()); }
    const_iterator cend() const { return const_iterator(mVec.end()); }

private:
    std::vector<const std::shared_ptr<MyClass>> mVec;
};
aschepler
  • 70,891
  • 9
  • 107
  • 161
  • If what I actually have is const shared_ptr, can the iterator_adaptor make operator* return a reference to const shared_ptr? fixme: shared_ptr doesn't allow this conversion (instead, it tries to construct a new shared_ptr) – cfa45ca55111016ee9269f0a52e771 Mar 04 '13 at 00:01
  • `iterator_adaptor::operator*` will return whatever you specify as the reference type. The constructor `shared_ptr::shared_ptr(const shared_ptr&)` (here with `T = const U`) will do the Right Thing, and the constructed pointer will share ownership with the original. – aschepler Mar 04 '13 at 13:00
  • I understand, but why is it allowed to return any type? Why can't I do the type cast by myself, but the boost library just bypasses the limitation so easily? I just want to have a reference const shared_ptr& point to an object const shared_ptr... – cfa45ca55111016ee9269f0a52e771 Mar 04 '13 at 13:25
  • 1
    You can't get a `const shared_ptr&` from a `const shared_ptr` because a `shared_ptr` is not a `shared_ptr`. If didn't specify a `reference_type` and allowed boost to use the default `const shared_ptr&`, it would have a compile error just as much. The version which makes `operator*` return by value works because the `return` statement implicitly constructs a copy `shared_ptr`. – aschepler Mar 04 '13 at 13:45
  • Then operator*() has to return b value. It means I don't have access to the original shared pointer anyway. But since it's const, in my case, this limitation doesn't really matter. The problem is that operator* returns be value, but I can solve that by using boost::indirect_iterator (i.e. iterators point to shared objects) and get shared pointers using std::enable_shared_from_this, when I really need them. Problem solved... – cfa45ca55111016ee9269f0a52e771 Mar 04 '13 at 13:53
1

shared_ptr and other standard smart pointers are not designed with deep-constness in mind. They are trying to be as close to raw pointer usage as possible, and raw pointer's const-ness does not affect the pointee's const-ness.

Andrei Alexandrescu's Loki::SmartPtr (described in his Modern C++ Design) implements reference counting and deep const-ness as policies, which would give you the effect you're looking for. If you don't mind switching to a non-standard smart pointer in order to get non-standard behavior, that may be one way to go.

metal
  • 6,202
  • 1
  • 34
  • 49
  • it's not non-standard behavior, what I'm looking for is very expected from a container of pointers – cfa45ca55111016ee9269f0a52e771 Mar 01 '13 at 18:27
  • 1
    No, it's not, it can only work if `vector::iterator` is just a typedef for `T*` but that's not guaranteed – Jonathan Wakely Mar 01 '13 at 18:28
  • It's non-standard in the sense that the C++ standard library's pointers don't support what you want out of the box. – metal Mar 01 '13 at 18:28
  • Some people want it, some people don't. If you want a drop-in replacement for raw pointers, you probably don't because it'll break your (const-correct) code. You could write your own deep_shared_ptr, which wraps up a shared_ptr but supplies const-correct accessors. – metal Mar 01 '13 at 18:30