简体   繁体   中英

How to support a range based loop in polymorphic classes (containing vector and set)?

I would like to iterate over some items in my class

for (const auto i: myclass) { /* do stuff with i */}

For this I want to expose the iterator of whatever STL container happens to be storing my data inside myclass .

My class is polymorphic and has the following hierarchy:

#include <set>
#include <vector>

class Base_t {
public:
    //works for the set, but not for the vector
    //using iterator_t = decltype(std::declval<std::set<int>>().cbegin());

    //does not work, see error below.
    using iterator_t = std::iterator<std::input_iterator_tag, int>;

    virtual iterator_t begin() = 0;
    virtual iterator_t end() = 0;
};

class MyVector_t: public Base_t {
    std::vector<int> v;
public:
    iterator_t begin() override { return v.begin(); }
    iterator_t end() override {return v.end(); }
};

class MyTree_t: public Base_t {
    std::set<int> s;
public:
    iterator_t begin() override { return s.begin(); }
    iterator_t end() override {return s.end(); }
};

//#################################################
//the class using these things looks like:
//#################################################
class Worker_t {
    Base_t& container;  //polymorphic container
public:
    Worker_t(const bool UseTree): container(*CreateContainer(UseTree)) {}

    Base_t* CreateContainer(const bool UseTree) const {
        if (UseTree) { return new MyTree_t(); }
        else         { return new MyVector_t(); }
    }

    //need an iterator into the container to use it.
    void DoStuff() { for (const auto i: container) { printf("%i",i); } } 
};

int main(const int argc, const char* argv[]) {
    const auto UseTree = true;
    Worker_t worker1(UseTree);
    worker1.DoStuff();

    Worker_t worker2(!UseTree);
    worker2.DoStuff();
}

This gives the error:

no viable conversion from returned value of type 'std::set::const_iterator' (aka '__tree_const_iterator<int, std::__tree_node<int, void *> *, long>') to function return type 'Base_t::it' (aka 'iterator<std::input_iterator_tag, int>')

I could make the hierarchy a CRTP setup, but I need the methods to be virtual.

template <typename T>
class Base_t<T> {
    ....
}; 

class Derived: Base_t<Derived> {
    ...
};

Does not work for me, because the code using the classes only knows about Base_t, not about anything else, that is, the class using these things looks like:

class Worker_t {
    Base_t& container;  //polymorphic container
public:
    Worker_t(const bool UseTree): container(*CreateContainer(UseTree)) {}
   
    Base_t* CreateContainer(const bool UseTree) const {
        if (UseTree) { return new MyTree_t(); }
        else         { return new MyVector_t(); }
    }

    //need an iterator into the container to use it.
    void DoStuff() { for (const auto i&: container) { /* do stuff*/ } } 
};

What would be the minimal changes needed to make the code work, preferably using virtual methods?

For the record I'm using c++20

Apple clang version 13.1.6 (clang-1316.0.19.2)
Target: x86_64-apple-darwin21.3.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

You're attempting to force runtime polymorphism into a box designed for compile-time polymorphism. That will naturally create problems.

The iterator mechanism is based on compile-time mechanisms being able to ask questions of the iterator. Iterator tag s are nothing like base classes. Iterator tag types do not do anything at runtime; they are solely used to allow compile-time metaprogramming (template or constexpr -based) to detect if an iterator's type provides the interface of a particular iterator category.

In runtime polymorphism, the base class defines everything. Everything , directly and explicitly. Derived classes can change the implementation, but they cannot change the compile-time interface.

An iterator's value_type (the type the iterator "points" to) is a compile-time construct. vector<int> has a different iterator type from vector<float> . They both model the same kind of iterator (random-access), but the types themselves have no runtime relationship. Iterator types are largely unaware of each other, even if they have the same iterator category and value_type .

std::array<int, 5> and std::vector<int> both have RandomAccessIterators over the same value_type , but the two iterator types have nothing in common. They can be used in the same compile-time polymorphic interfaces, but they cannot be used interchangeably at runtime.

Iterator categories are not base classes .

Since the base class defines a virtual interface which returns a concrete type, all properties of such iterators are defined by the base class . Derived classes can provide the actual range of values, but all compile-time aspects of the iterators must be fixed at compile time. This includes the underlying type of the iterator.

If you are fine with restricting all of your derived classes to using a particular container with a particular iterator, that's fine. But if you want them to define their own containers with different iterator types, that's a problem.

The only way around this is to create a type-erased iterator type for your base class, which can be filled in by any iterator and itself uses runtime polymorphism to access the real iterator type. This isn't trivial to implement and involves significant overhead (as every operation on the iterator is now a dynamic dispatch of some form).

Also, such an iterator can only ever model a specific kind of iterator. So if you want derived classes to be able to store data in a RandomAccessIterator or a ForwardIterator, your type-erased iterator must only expose itself as a ForwardIterator: the lowest common denominator.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM