Search code examples
c++c++11templatescastingshared-ptr

C++ Cast to "uncle class"


I have a Wrapper<T> template class.

I have a base class A.

I have a derived class B : public A.

What I want is to store a Wrapper<B> in a vector<Wrapper<A>>.

I know this technically not right as Wrapper<B> is not a direct subclass of Wrapper<A>

See inheritance diagram

But somehow, STL allow that for shared_ptr:

#include <memory>
#include <vector>
#include <iostream>

struct A
{
  A() {}
  virtual ~A() {}
  virtual void print() { std::cout << "A" << std::endl; }
};

struct B : public A
{
  B() {}
  virtual ~B() {}
  virtual void print() { std::cout << "B" << std::endl; }
};

int main()
{
  std::vector<std::shared_ptr<A>> v;

  v.push_back(std::make_shared<A>());
  v.push_back(std::make_shared<B>());

  for (auto &e : v)
    e->print();

  return 0;
}

How can I achieve the same result for my Wrapper<T> class ?

This is the non-compiling code:

#include <vector>
#include <iostream>

template <class T>
struct W
{
};

struct A
{
  A() {}
  virtual void print() { std::cout << "A" << std::endl; }
};

struct B : public A
{
  B() {}
  virtual void print() { std::cout << "B" << std::endl; }
};

int main()
{
  std::vector<W<A>> v;

  v.push_back(W<A>());
  v.push_back(W<B>());

  for (auto &e : v)
    e->print();

  return 0;
}

Best, Pierre


Solution

  • Shared ptr to A and B are unrelated types.

    What shared ptr does is provide a converting constructor. So long as the pointer-types can be converted, it accepts both raw-pointer-to and shared-ptr-to.

    Shared ptr type erases how to do the final destruction, and stores the pointer, so this really isn't hard.

    How exactly this works will depend on how the data is stoted and what Wrapper does.

    But:

    template<class T>
    struct Wrapper {
      template<class U>
      Wrapper(Wrapper<U>);
    };
    

    is basically it. Maybe some SFINAE to get earlier errors.

    ...

    As you have actually added details in comments, you have:

    template<class T>
    struct RollbackElement : public T
    

    as Wrapper. The answer is, no, you cannot do that.

    You can do:

    template<class T>
    struct RollbackElement : public std::any {
      operator T&() {
        return std::any_cast<T&>(*this);
      }
    };
    

    where instead of storing an actual T, you store a type-erasing wrapper around a T.

    template<class T>
    struct RollbackElement : private std::any {
      operator T&() {
        return *get();
      }
      operator T const&() const {
        return *get();
      }
      RollbackElement( T in ):
        std::any(std::move(in)),
        get_impl([](std::any& x)->T*{return std::any_cast<T*>(&x);})
      {}
      RollbackElement():RollbackElement({}) {}
      template<class U>
      RollbackElement( RollbackElement<U> const& o ):
        std::any( static_cast<std::any const&>(o) ),
        get_impl(o.get_impl)
      {}
      // note: due to quirks in `std::any`, a move ctor isn't safe
      T* operator->() { return get(); }
      T const* operator->() const { return get(); }
      T* get() { return get_impl(*this); }
      T const* get() const { return get_impl(const_cast<std::any&>(*this)); }
    private:
      std::function<T*(std::any&)> get_impl = {};
    };
    

    plus whatever augmentation you want on it.

    The trick here is that your storage, a std::any, can store anything.

    We store how to get from std::any to a T* in get_impl.

    When we copy a RollbackElement we copy both the any (without casting to a concrete type) and the get_impl. If the other Rollback is a different type, we rely on the converting constructor of std::function to adjust the returned pointers for us.