This doesn't work because
- you are not returning pointers or references, which is required for covariant returns to work; and
Foo<B>
and Foo<B>
have no inheritance relationship regardless of Foo
, A
and B
(unless there's a specialization that makes it so).
But we can work around that. First, note that std::vector<A*>
and std::vector<B*>
are not substitutable for each other, regardless of any language restrictions, simply because std::vector<B*>
cannot support adding an A*
element to it. So you cannot even write a custom adapter that makes std::vector<B*>
a substitute of std::vector<A*>
But a read-only container of B*
can be adapted to look like a read-only container of A*
. This is a multi-step process.
Create an abstract class template that exports a readonly container-like interface
template <class ApparentElemType>
struct readonly_vector_view_base
{
struct iter
{
virtual std::unique_ptr<iter> clone() const = 0;
virtual ApparentElemType operator*() const = 0;
virtual iter& operator++() = 0;
virtual iter& operator--() = 0;
virtual bool operator== (const iter& other) const = 0;
virtual bool operator!= (const iter& other) const = 0;
virtual ~iter(){}
};
virtual std::unique_ptr<iter> begin() = 0;
virtual std::unique_ptr<iter> end() = 0;
virtual ~readonly_vector_view_base() {}
};
It return pointers to iterators, not iterators themselves, but
don't worry, this class will be only used by an STL-like wrapper anyway.
Now create a concrete wrapper for readonly_vector_view_base
and its iterator, so that it contains a pointer to, and delegate its operations to, a readonly_vector_view_base
.
template <class ApparentElemType>
class readonly_vector_view
{
public:
readonly_vector_view(const readonly_vector_view& other) : pimpl(other.pimpl) {}
readonly_vector_view(std::shared_ptr<readonly_vector_view_base<ApparentElemType>> pimpl_) : pimpl(pimpl_) {}
typedef typename readonly_vector_view_base<ApparentElemType>::iter iter_base;
class iter
{
public:
iter(std::unique_ptr<iter_base> it_) : it(it_->clone()) {}
iter(const iter& other) : it(other.it->clone()) {}
iter& operator=(iter& other) { it = other.it->clone(); return *this; }
ApparentElemType operator*() const { return **it; }
iter& operator++() { ++*it; return *this; }
iter& operator--() { --*it; return *this; }
iter operator++(int) { iter n(*this); ++*it; return n; }
iter operator--(int) { iter n(*this); --*it; return n; }
bool operator== (const iter& other) const { return *it == *other.it; }
bool operator!= (const iter& other) const { return *it != *other.it; }
private:
std::unique_ptr<iter_base> it;
};
iter begin() { return iter(pimpl->begin()); }
iter end() { return iter(pimpl->end()); }
private:
std::shared_ptr<readonly_vector_view_base<ApparentElemType>> pimpl;
};
Now create a templatized implementation for readonly_vector_view_base
that looks at a vector of a differently typed elements:
template <class ElemType, class ApparentElemType>
struct readonly_vector_view_impl : readonly_vector_view_base<ApparentElemType>
{
typedef typename readonly_vector_view_base<ApparentElemType>::iter iter_base;
readonly_vector_view_impl(std::shared_ptr<std::vector<ElemType>> vec_) : vec(vec_) {}
struct iter : iter_base
{
std::unique_ptr<iter_base> clone() const { std::unique_ptr<iter_base> x(new iter(it)); return x; }
iter(typename std::vector<ElemType>::iterator it_) : it(it_) {}
ApparentElemType operator*() const { return *it; }
iter& operator++() { ++it; return *this; }
iter& operator--() { ++it; return *this; }
bool operator== (const iter_base& other) const {
const iter* real_other = dynamic_cast<const iter*>(&other);
return (real_other && it == real_other->it);
}
bool operator!= (const iter_base& other) const { return ! (*this == other); }
typename std::vector<ElemType>::iterator it;
};
std::unique_ptr<iter_base> begin() {
iter* x (new iter(vec->begin()));
std::unique_ptr<iter_base> y(x);
return y;
}
std::unique_ptr<iter_base> end() {
iter* x (new iter(vec->end()));;
std::unique_ptr<iter_base> y(x);
return y;
}
std::shared_ptr<std::vector<ElemType>> vec;
};
OK, as long as we have two types where one is convertible to another, such as A*
and B*
, we can view a vector of B*
as if it's a vector of A*
.
But what does it buy us? readonly_vector_view<A*>
is still unrelated to readonly_vector_view<B*>
! Read on...
It turns out that the covariant return types are not really necessary, they are a syntactic sugar to what is available in C++ otherwise. Suppose C++ doesn't have covariant return types, can we simulate them? It's actually pretty easy:
class Base
{
virtual Base* clone_Base() { ... actual impl ... }
Base* clone() { return clone_Base(); } // note not virtual
};
class Derived : public Base
{
virtual Derived* clone_Derived() { ... actual impl ... }
virtual Base* clone_Base() { return clone_Derived(); }
Derived* clone() { return clone_Derived(); } // note not virtual
};
It's actually pretty easy and there's no requirement for the return type to be pointers or references, or have an inheritance relationship. It is enough that there is a conversion:
class Base
{
virtual shared_ptr<Base> clone_Base() { ... actual impl ... }
shared_ptr<Base> clone() { return clone_Base(); }
};
class Derived : public Base
{
virtual shared_ptr<Derived> clone_Derived() { ... actual impl ... }
virtual shared_ptr<Base> clone_Base() { return clone_Derived(); }
shared_ptr<Derived> clone() { return clone_Derived(); }
};
In a similar fashion, we can arrange A::test()
to return a readonly_vector_view<A*>
, and B::test()
to return a readonly_vector_view<B*>
. Since these functions are now not virtual, there is no requirement for their return types to be in any relationship. One just hides the other. But inside they call a virtual function that creates (say) a readonly_vector_view<A*>
implemented in terms of readonly_vector_view_impl<B*, A*>
which is implemented in terms of vector<B*>
, and everything works just as if they were real covariant return types.
struct A
{
readonly_vector_view<A*> test() { return test_A(); }
virtual readonly_vector_view<A*> test_A() = 0;
};
struct B : A
{
std::shared_ptr<std::vector<B*>> bvec;
readonly_vector_view<B*> test() { return test_B(); }
virtual readonly_vector_view<A*> test_A() {
return readonly_vector_view<A*>(std::make_shared<readonly_vector_view_impl<B*, A*>>(bvec));
}
virtual readonly_vector_view<B*> test_B() {
return readonly_vector_view<B*>(std::make_shared<readonly_vector_view_impl<B*, B*>>(bvec));
}
};
Piece of cake! Live demo Totally worth the effort!
B b = ...; vector<A*> x = b.test();
and now we you can addA*
objects to a vector ofB
s, which breaks the guarantees ofvector<B*>
. However my knowledge of C++ is very limited. – Kendakendal