Home > other >  Pure virtual method taking all sorts of iterators?
Pure virtual method taking all sorts of iterators?

Time:11-18

Let a polymorphic Base class have a pure virtual method insert on a stl container member (vector in this case). The function should be able to take iterator of containers like set, vector, list etc, but also take into account the type of reference (move semantics)

The pure virtual nature of the function make using template functions impossible. Afaik iterators of stl containers are separate type, thats why templates are useful. However the polymorphism is necessary. Also I noticed that there is std::move_iterator which is able to encapsulate all types of iterators.

Are there other "iterator wrappers" which I can use to define a pure virtual method in Base which takes all sorts of iterators and also acts like perfectly forwarding function template such that clients can pass iterators and move iterators?

Before introducing polymorphism the function was like this:

vector<Class> v;
template<typename Iter>
void insert(Iter begin, Iter end) {
    v1.insert(begin, end, std::end(v));
}

but now there derived classes which behave slightly different on insert (mutexes, notify observers etc). It would be nice to have something like the following:

vector<Class> v;

virtual void Base::insert(GenericIter begin, GenericIter end) = 0;

[…]

void DerivedMT::insert(GenericIter begin, GenericIter end) override
{
    mutex.lock();
    v1.insert(begin, end, std::end(v));
    mutex.unlock();
}

[…]

void DerivedObserved::insert(GenericIter begin, GenericIter end) override
{
    v1.insert(begin, end, std::end(v));
    notifyObservers();
}

CodePudding user response:

You can't accept all the various iterators and maintain runtime polymorphism because you would then violate Liskov Substitution Principle. That is, a polymorphic bidirectional iterator will not work with wrapped forward iterator, since the latter can only be incremented. Also there are sorted associative containers, whose iterators you can not use for sorting, etc:

  • in case an implementor would want not to skip, but to sort the elements
  • in case an implementor would want to do a particular kind of search

So, the intent of a template iterator for a function is to provide a freedom for an interface implementor to do whatever he desires given he has two iterators. But with runtime polymorphic iterators you are limiting the implementor (you kind of should).

So there are two ways:

  1. Naive. Just declare your interface with insert(std::vector<YourType>). This will handle for your all the generic iterators you want, and an implementor is free to do whatever he desires with the range.
  2. Implement const PolymorphicForwardIterator using traits for a Forward Iterator and type erasure.

Here is an example of how you can erase a type without heap allocations:

class PolymorphicReference final
{
public:
    template <typename RefT>
    explicit PolymorphicReference(RefT &ref)
        : pVTable_(std::addressof(GetVTable(ref))),
          ref_(std::addressof(ref))
    {}

    void Say(const std::string& msg)
    {
        pVTable_->pSay(ref_, msg);
    }

    int Number() const
    {
        return pVTable_->pNumber(ref_);
    }

private:
    struct VTable
    {
        virtual void pSay(void *pRef, const std::string& msg) = 0;
        virtual int pNumber(const void *pRef) = 0;

    protected:
        ~VTable() = default;
    };

    template <typename RefT>
    static VTable &GetVTable(const RefT&)
    {
        struct : VTable
        {
            void pSay(void *pRef, const std::string& msg) override
            {
                static_cast<RefT*>(pRef)->Say(msg);
            }
            int pNumber(const void *pRef) override
            {
                return static_cast<const RefT*>(pRef)->Number();
            }
        } static vTable;

        return vTable;
    }

private:
    VTable *pVTable_;
    void *ref_;
};

CodePudding user response:

To complement the other answer, here's something to get you up and running on implementing a polymorphic iterator if that's the desired solution. Rather than a forward iterator, I modelled an input iterator since that's all you need when taking a pair as a range to insert. The full sample can be found here.

Do note that this type fully models std::input_iterator, which means that it can be used anywhere any other input iterator can be used.

template<typename T>
class any_const_input_iterator_of {
    // Store the actual iterator without its type.
    std::any _erased;

    // Store each operation we need from the actual iterator. 
    // Not ideal for space, but works as a starter example.
    void(*_increment)(std::any& erased);
    auto(*_deref)(const std::any& erased) -> const T&;
    auto(*_equals)(const std::any& erased, const std::any& erased_other) -> bool;

public:
    // Some required types to satisfy the iterator requirements.
    using iterator_category = std::input_iterator_tag;
    using difference_type = std::ptrdiff_t;
    using value_type = const T;

    // The constructor takes the real iterator. 
    // Since it knows the type, it can use the type in lambdas to fill in the operations.
    // Normally, you wouldn't squish the important parts here.
    template<std::input_iterator Iter> 
      requires std::equality_comparable<Iter> and
               std::same_as<T, std::iter_value_t<Iter>> // We don't want a const T& return to be a temporary
    any_const_input_iterator_of(Iter iter) :
      _erased(iter),
      _increment([](std::any& erased) {   std::any_cast<Iter&>(erased); }),
      _deref([](const std::any& erased) -> const T& { return *std::any_cast<Iter>(erased); }),
      _equals([](const std::any& erased, const std::any& erased_other) { 
          assert(erased.type() == erased_other.type() and "Erased iterator types differ. This is probably a bug.");
          return std::any_cast<Iter>(erased) == std::any_cast<Iter>(erased_other); 
      }) {}

    // Now that we have operations, we can use them to implement the iterator requirements.

    auto operator  () -> any_const_input_iterator_of& {
        _increment(_erased);
        return *this;
    }

    auto operator  (int) -> any_const_input_iterator_of {
        auto copy = *this;
          *this;
        return copy;
    }

    auto operator*() const -> const T& {
        return _deref(_erased);
    }

    auto operator->() const -> const T* {
        return &**this;
    }

    friend auto operator==(const any_const_input_iterator_of& lhs, const any_const_input_iterator_of& rhs) -> bool {
        return lhs._equals(lhs._erased, rhs._erased);
    }
};

static_assert(std::input_iterator<any_const_input_iterator_of<int>>);

There are two interesting things you might be wondering about. First, what about comparing with a sentinel? Well, I'm not sure this is even possible. The type of the sentinel, which needs to be available inside the lambda when calling operator==, is known only inside this class's operator==. There's no way to have both the iterator type and the sentinel type both known in the same place. If you limit sentinels to a finite list of types (as I did by requiring it to be the same type as the iterator), then your options open up a bit more.

Second, what about iterators whose value type is convertible to this one instead of an exact match? What if I want to pass set<double>::iterators as any_const_input_iterator_of<int>? Well, the issue here is lifetime. If dereferencing produces a double& and _deref returns const int&, then you're creating a temporary int that goes out of scope when _deref is done. Of course you can choose to return by value here and make copies. The problem really comes when the types do match. In this case, references are fine, but returning by value denies that. This is one of those things you have to live with when deciding to erase type information.

CodePudding user response:

Are there other "iterator wrappers" which I can use to define a pure virtual method in Base which takes all sorts of iterators and also acts like perfectly forwarding function template such that clients can pass iterators and move iterators?

No. Either you can have a virtual function, or you can have perfect forwarding, you can't have both.

What you can have is a type-erasing range, such as boost::any_range.

class Base {
public:
    virtual void insert(boost::any_range<Class, std::input_iterator_tag> range) = 0;
};
  •  Tags:  
  • c
  • Related