Interfaces and covariance problem

前端 未结 5 2039
我在风中等你
我在风中等你 2021-01-05 07:29

I have a particular class that stores a piece of data, which implements an interface:

template
class MyContainer : public Container

        
相关标签:
5条回答
  • 2021-01-05 08:05

    I would suggest a look at the Visitor pattern.

    Other than that, what you want is a value type that will be imbued with polymorphic behavior. There is a much simpler solution than James' using your IInterface.

    class IInterface
    {
      virtual ~IInterface() {}
      virtual void next() = 0;
      virtual void previous() = 0;
      virtual T* pointer() const = 0;
    
      virtual std::unique_ptr<IInterface> clone() const = 0;
    };
    
    std::unique_ptr<IInterface> clone(std::unique_ptr<IInterface> const& rhs) {
      if (!rhs) { return std::unique_ptr<IInterface>(); }
      return rhs->clone();
    }
    
    class Iterator
    {
      friend class Container;
    public:
      Iterator(): _impl() {}
    
      // Implement deep copy
      Iterator(Iterator const& rhs): _impl(clone(rhs._impl)) {}
      Iterator& operator=(Iterator rhs) { swap(*this, rhs); return *this; }
    
      friend void swap(Iterator& lhs, Iterator& rhs) {
        swap(lhs._impl, rhs._impl);
      }
    
      Iterator& operator++() { assert(_impl); _impl->next(); return *this; }
      Iterator& operator--() { assert(_impl); _impl->previous(); return *this; }
      Iterator operator++(int); // usual
      Iterator operator--(int); // usual
    
      T* operator->() const { assert(_impl); return _impl->pointer(); }
      T& operator*() const { assert(_impl); return *_impl->pointer(); }
    
    private:
      Iterator(std::unique_ptr<IInterface> impl): _impl(impl) {}
      std::unique_ptr<IInterface> _impl;
    };
    

    And finally, the Container class will propose:

    protected:
      virtual std::unique_ptr<IInterface> make_begin() = 0;
      virtual std::unique_ptr<IInterface> make_end() = 0;
    

    And implement:

    public:
      Iterator begin() { return Iterator(make_begin()); }
      Iteraotr end() { return Iterator(make_end()); }
    

    Note:

    You can do away with the std::unique_ptr if you can avoid the ownership issue. If you can restrict the IInterface to be behavioral only (by extracting the state into Iterator), then you can have the Strategy pattern kick-in, and use a pointer a statically allocated object. This way, you avoid dynamic allocation of memory.

    Of course, it means your iterators won't be so rich, as it requires IInterface implementations to be stateless, and implementing "filtering" iterators, for example, would become impossible.

    0 讨论(0)
  • 2021-01-05 08:06

    Have you thought about using CRTP. I find it a good candidate here. Here is a brief demo. It just explains your ++retval problem (if I understood it correctly). You have to change your IInterface definition from pure virtual to CRTP type interface.

    template<class Derived>
    struct IInterface
    {
      Derived& operator ++ ()
      {
        return ++ *(static_cast<Derived*>(this));
      }
    };
    
    struct Something : public IInterface<Something>
    {
      int x;
      Something& operator ++ ()
      {
        ++x;
        return *this;
      }
    };
    

    There are some limitations of CRTP, that the template will always follow your IInterface. Which means that if you are passing a Something object to a function like this:

    foo(new Something);
    

    Then, foo() should be defined as:

    template<typename T>
    void foo(IInterface<T> *p)
    {
      //...
      ++(*p);
    }
    

    However for your problem, it can be a good fit.

    0 讨论(0)
  • 2021-01-05 08:13

    What you are trying to do is called type erasure. Basically you want to provide a value type (which is the same across the whole inheritance hierarchy) that wraps the particular iterator type and offers a uniform dynamic interface.

    Type erasure is usually implemented with a non-virtual class (the type erased) that stores a pointer to a virtual base class that implements the erasure, from which you derive different types that wrap each particular iterator. The static class would offer templated constructor/assignment operators that would dynamically instantiate an object of the derived type and store the pointer internally. Then you only need to implement the set of operations as dispatch to the internal object.

    For the simplest form of type erasure possible, you can take a look at the implementation of boost::any (documentation is here)

    Sketch:

    namespace detail {
       template<typename T>
       struct any_iterator_base {
          virtual T* operator->() = 0;    // Correct implementation of operator-> is tough!
          virtual T& operator*() = 0;
          virtual any_iterator_base& operator++() = 0;
       };
       template <typename T, typename Iterator>
       class any_iterator_impl : any_iterator_base {
          Iterator it;
       public:
          any_iterator_impl( Iterator it ) : it(it) {}
          virtual T& operator*() {
             return *it;
          }
          any_iterator_impl& operator++() {
             ++it;
             return *this;
          }
       };
    }
    template <typename T>
    class any_iterator {
       detail::any_iterator_base<T>* it;
    public:
       template <typename Iterator>
       any_iterator( Iterator it ) : it( new detail::any_iterator_impl<T,Iterator>(it) ) {}
       ~any_iterator() {
          delete it;
       }
       // implement other constructors, including copy construction
       // implement assignment!!! (Rule of the Three)
       T& operator*() {
          return *it;   // virtual dispatch
       }
    };
    

    The actual implementation becomes really messy. You need to provide different versions of the iterator for the different iterator types in the standard, and the detail of the implementation of the operators might not be trivial either. In particular operator-> is applied iteratively until a raw pointer is obtained, and you want to make sure that your type erased behavior does not break that invariant or document how you break it (i.e. limitations on the type T that your adaptor can wrap)

    For extended reading: - On the Tension Between Object-Oriented and Generic Programming in C++ - any_iterator: Implementing Erasure for C++ iterators - adobe any_iterator ,

    0 讨论(0)
  • 2021-01-05 08:18

    Like you said, the problem is that instances of Something are tied to the object it holds. So let's try to untie them.

    The key point to remember is that in OOP, public non-const data members are generally frowned upon. In your current implementation, every Something instance is tied to having a data member T x which is publicly accessible. Instead of this, is considered better to make an abstraction of this, i.e. provide accessor methods instead:

    class Something : IInterface
    {
    private:
        T x;
    
    public:
        T GetX()
        {
            return x;
        }
    };
    

    Now the user has know idea what type of thing x is, much less that x exists.

    This is a good first step, however, since you wish be able to have x refer to different objects at different times, we're pretty much going to have to make x be a pointer. And as a concession to conventional code, we'll also make GetX() return a const reference, rather than a regular value:

    class Something: IInterface
    {
    private:
        T *x;
    
    public:
        T const& GetX()
        {
            return *x;
        }
    };
    

    It's now trivial to implement the methods in IInterface:

    class Something: IInterface
    {
    private:
       T *x;
    
    public:
        T const& GetX()
        {
            return *x;
        }
    
        T& operator*()
        {
            return *x;
        }
    
        T* operator->()
        {
            return x;
        }
    
        Something& operator++()
        {
            ++x;
            return *this;
        }
    };
    

    The ++ operator is trivial now - it really just applies the ++ to x.

    The user now has no idea that a pointer was used. All they know is that their code works right. That's the most important point in OOP's principle of data abstraction.

    Edit

    As far as implementing the begin and end methods of Container, that shouldn't be too difficult either, but it will require some changes to Container.

    First off, let's add a private constructor to Something which takes a pointer to the starting object. We'll also make MyContainer a friend of Something:

    class Something: IInterface {

        friend class MyContainer; // Can't test the code right now - may need to be MyContainer<T> or ::MyContainer<T> or something.
    
    private:
       T *x;
    
        Something( T * first )
        : x(first)
        {
        }
    
    public:
    
        T const& GetX()
        {
            return *x;
        }
    
        T& operator*()
        {
            return *x;
        }
    
        T* operator->()
        {
            return x;
        }
    
        Something& operator++()
        {
            ++x;
            return *this;
        }
    };
    

    By making the constructor private, and setting the friend dependancy, we ensure that only MyContainer can make new Something iterators (this protects us iterating over random memory if something erroneous were given by a user).

    Next off, we'll change MyContainer a little, so that rather than having an array of Something, we'll just have an array of T:

    class MyContainer
    {
        ...
    private:
    
        T *data;
    
    };
    

    Before we get to implementing begin and end, let's make that change to Container I talked about:

    template<typename T, typename IteratorType>
    class Container {
    public:
        ...
        // These prototype are the key. Notice the return type is IteratorType (value, not reference)
        virtual IteratorType begin() = 0;
        virtual IteratorType end() = 0;
    };
    

    So rather than relying on covariance (which would be really difficult in this case), we use a little template magic to do what we want.

    Of course, since Container now accepts another type parameter, we need a corresponding change to MyContainer; namely we need to provide Something as the type parameter to Container:

    template<class T>
    class MyContainer : Container<T, Something>
    ...
    

    And the begin/end methods are now easy:

    template<class T>
    MyContainer<T>::begin()
    {
        return Something(data);
    }
    
    template<class T>
    MyContainer<T>::end()
    {
        // this part depends on your implementation of MyContainer.
        // I'll just assume your have a length field in MyContainer.
        return Something(data + length);
    }
    

    So this is what I've got for my midnight thinking. Like I mentioned above, I cannot currently test this code, so you might have to tweak it a bit. Hopefully this does what you want.

    0 讨论(0)
  • 2021-01-05 08:18

    If the usage is supposed to be similar to stdlib, then the iterator needs to be a value object, because it normally gets copied by value a lot. (Also, otherwise what would the begin and end method return a reference to?)

    template <class T>
    class Iterator
    {
        shared_ptr<IIterator> it;
    public:
        Iterator(shared_ptr<IIterator>);
        T& operator*() { it->deref(); }
        T* operator->() { return &it->deref(); }
        Iterator& operator++() { it->inc(); return *this; }
        etc.
    };
    
    0 讨论(0)
提交回复
热议问题