从void*进行多态转换

5
请参考下面的代码以了解详情,但基本情况如下。我有一个容器(会话),可以将对象放入其中并从中取出。
类似于:
 std::shared_ptr<Tiger> t = ...;
 session.store("tigers/1", t);

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

两个函数定义如下:

 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);
 }

请注意,一个会话可以存储异构类型,但在存储和加载时,我静态地知道变量的类型。我的问题是,我遇到了这样一种情况:用户想把一个“Tiger”放入会话,但却检出了一个基本类型。例如:
  session.load<Animal>("tigers/1");

目前,我在会话中有效地将数据存储为void*并使用reinterpret_cast将它们转换回用户提供的类型。这个...可以工作,只要一切都是简单的,但当我们遇到稍微复杂一些的情况时,就会遇到问题。

这里是完整的代码,演示了我的问题:

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;
}

如果你运行这段代码,你会发现它会运行,但是它会调用 Pet 而不是调用 Jump。我明白这是因为使用了错误的虚函数表,因为实际上我在对 `void*` 进行了 reinterpret_cast
我的问题是,有没有一种好的方式来处理 C++ 中的这种情况。我查找了一些资料,但没有看到任何与我需要的内容相匹配的东西。
我所找到的关于异构容器的所有内容都假定存在共享的基类,而我既没有也不想要这样的基类。这可能吗?

Session类中,您保证只存储Animal的基本类型吗? - Kunal Puri
如果您事先知道要存储的类型,也许您想看看std::variant。 - grungegurunge
3
存储的类型是否在静态时已知?如果是,您可以使用std::variant吗?如果不是,您可以使用std::any吗? - Krzysiek Karbowiak
1
@KrzysiekKarbowiak 我无法使用 std::any,因为类型不匹配。`auto cat = new Tiger();auto any_ptr = std::any(cat);auto namable = std::any_cast<IJumpable*>(any_ptr); `这将导致错误的转换。 - Ayende Rahien
1
我可以存储类型,但我不知道如何做到这一点或如何正确地取出它。我考虑使用lambda来捕获类型,但我不知道如何将目标类型正确地传递给它。 - Ayende Rahien
显示剩余7条评论
3个回答

6
你可以让用户提供正确的投射路径:
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);
      }
 }

这使得调用方使用起来很混乱,但这些信息必须从某处获取:
auto shere_khan = make_shared<Tiger>();
session.store("tigers/1", shere_khan);
auto bagheera = session.load_as<Tiger, IJumpable>("tigers/1");

这种方式能存储原始数据类型吗? - Kunal Puri
好的。明白了。谢谢。 - Kunal Puri

3

这是我兄弟提供的解决方案,他是一位没有使用stackoverflow的C++专家 :)

这里有一个void_ptr实现,可以通过异常处理来发现类型以实现多态转换。性能应该接近于dynamic_cast。您应该能够使用std::type_index并缓存偏移量来优化上述内容。

#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());
}

哇,我不确定我是敬畏还是害怕这段代码。这需要一些冥想。非常酷。 - Ayende Rahien
感谢@AyendeRahien - 更新了代码,抛出和捕获指针而不是引用,因为使用引用会调用复制构造函数。 - Ajai

0

在我看来,最好的解决方案不是将其转换为指向 void 的指针,而是转换为其他类型,可以通过动态侧向转换到所需的类型。

#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;
}

实时示例

其他解决方案需要使用std::any,但这会不太方便。

令人有些不安的是你的方法load是一个模板。


1
我的问题是我无法控制提供给我的类型(它们由调用者完成,而我不想限制它们)。 - Ayende Rahien
你会如何为 load 方法提供类型,因为它需要能够返回任何类型? - Ayende Rahien

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接