Ayende Rahien
Ayende Rahien

Reputation: 22956

Polymorphic casting from void*

See code below for the details, but the underlying scenario is as follows. I have a container (a session) that I can place objects in and pull out from.

Similar to:

 std::shared_ptr<Tiger> t = ...;
 session.store("tigers/1", t);

 std::shared_ptr<Tiger> t2 = session.load<Tiger>("tigers/1");

With both functions defined as:

 class Session { 
      template<class T> 
      void store(std::string id, std::shared_ptr<T> instance);

      template<class T> 
      std::shared_ptr<T> load(std::string id);
 }

Note that a session can store heterogeneous types, but at store and load time I statically known what the type of the variable is.

My problem is that I run into a situation where the user wants to put a Tiger into the session but checks out a base type, instead. For example:

  session.load<Animal>("tigers/1");

Right now, I'm effectively storing the data as void* inside the session and use reinterpret_cast to get them back to the user provided type. This... works, as long as everything is trivial, but when we get to a slightly more complex situation, we run into issues.

Here is the full code demonstrating my issue:

struct Animal
{
    virtual void Pet() const = 0;
};

struct IJumpable
{
    virtual void Jump() const = 0;
};

struct Tiger : Animal, IJumpable
{
    void Pet() const override
    {
        std::cout << "Pet\n";
    }

    void Jump() const override
    {
        std::cout << "Jump\n";
    }
};

int main()
{
    auto cat = std::make_shared<Tiger>();

    // how the data is stored inside the session
    auto any_ptr = std::static_pointer_cast<void>(cat);
    // how we get the data out of the session
    auto namable = std::static_pointer_cast<IJumpable>(any_ptr);

    namable->Jump();
    std::cout << std::endl;
}

If you run this code, you'll see that it runs, but instead of calling Jump, it calls to Pet. I understand that this is because of the wrong virtual method table being used, since I'm effectively calling reinterpret_cast on `void*.

My question is if there is a good way to handle this scenario in C++. I've looked around and didn't see anything that matches what I need.

Everything I found about heterogeneous containers always assumed a shared base class, which I don't have nor want. Is this possible?

Upvotes: 6

Views: 993

Answers (3)

Ajai
Ajai

Reputation: 338

Solution courtesy of my brother who happens to be a C++ expert with no stackoverflow :)

Here is a void_ptr implementation that enables polymorphic casting using exception handling to discover types. The performance should be close to that of a dynamic_cast. You should be able to optimize the above using std::type_index and caching the offsets.

#include <stdio.h>

class void_ptr {
  void* obj;
  void (*discover_type)(void*);

  template<typename T>
  static void throw_typed_object(void* obj)
  {
    T* t = static_cast<T*>(obj);
    throw t;
  }
public:

  void_ptr() : obj(0) {}

  template<typename T>
  void_ptr(T* t) : obj(t), discover_type(throw_typed_object<T>)
  {
  }

  template<typename T>
  T* cast() const
  {
    try {
      discover_type(obj);
    } catch(T* t) {
      return t;
    } catch(...) {
    }
    return 0;
  }
};

struct Animal {
  virtual ~Animal() {}
  virtual const char* name() { return "Animal"; }
};

struct Speaker {
  virtual ~Speaker() {}
  virtual const char* speak() { return "hello"; }
};

struct Lion : public Animal, public Speaker {
  virtual const char* name() { return "Lion"; }
  virtual const char* speak() { return "Roar"; }
};


int main()
{
  void_ptr ptr(new Lion());


  Animal* a = ptr.cast<Animal>();
  Speaker* s = ptr.cast<Speaker>();

  printf("%s\n", a->name());
  printf("%s\n", s->speak());
}

Upvotes: 3

YSC
YSC

Reputation: 40080

You could make the user provide you with the correct casting trek to follow:

class Session { 
      template<class T> 
      void store(std::string id, std::shared_ptr<T> instance);

      template<class T> 
      std::shared_ptr<T> load(std::string id);

      template<class Stored, class Retrieved>
      std::shared_ptr<Retrieved> load_as(std::string id) {
          auto stored = load<Stored>(id);
          return std::static_pointer_cast<Retrieved>(stored);
      }
 }

This makes a messy usage at the caller site, but the information must come from somewhere:

auto shere_khan = make_shared<Tiger>();
session.store("tigers/1", shere_khan);
auto bagheera = session.load_as<Tiger, IJumpable>("tigers/1");

Upvotes: 7

Marek R
Marek R

Reputation: 37927

IMO best solution is not to cast to pointer to void but to other type which can be dynamic sidecast to required type.

#include <iostream>
#include <memory>

struct Animal
{
    virtual ~Animal() {}
    virtual void Pet() const = 0;
};

struct IJumpable
{
    virtual ~IJumpable() {}
    virtual void Jump() const = 0;
};

struct IStrorable
{
    virtual ~IStrorable() {}
};

struct Tiger : Animal, IJumpable, IStrorable
{
    void Pet() const override
    {
        std::cout << "Pet\n";
    }

    void Jump() const override
    {
        std::cout << "Jump\n";
    }
};

int main()
{
    auto cat = std::make_shared<Tiger>();

    auto any_ptr = std::static_pointer_cast<IStrorable>(cat);

    auto namable = std::dynamic_pointer_cast<IJumpable>(any_ptr);

    namable->Jump();
    std::cout << std::endl;
}

Live example

Other solutions require use of std::any, but this will be less handy.

It is a bit disturbing that your method load is a template.

Upvotes: 0

Related Questions