如何编写自己的dynamic_cast函数

5

这是面试中被问到的问题。

如何编写自己的dynamic_cast。我认为,可以基于typeid的name函数。

现在该如何实现自己的typid?我对此毫无头绪。


@saurabh01 请回顾您所提出的问题,并选择最有帮助的答案(如果有的话)。单击该答案左侧的复选框以接受它。谢谢! - Owen S.
14
本月最糟糕的面试问题之一。 - anon
2
@Neil Butterworth:我同意。编译器实现DC的原因是有道理的。 - Puppy
2
@Ammon 编译器不能做你可以自行实现的任何操作。像“编译器如何实现dynamic_cast?”这样的问题可能会更好,但仍然只对C ++编译器实现者有兴趣。一个好的问题应该是类似于“什么时候会使用dynamic_cast?”的东西。 - anon
我实际上已经完成了这个任务,以实现跨DLL安全和跨编译器安全的实现。 - iAdjunct
显示剩余4条评论
4个回答

20

你不知道的原因是,dynamic_caststatic_cast不像const_cast或者reinterpret_cast,它们实际上执行指针算术并且有一定的类型安全性。

指针算术

为了说明这一点,考虑以下设计:

struct Base1 { virtual ~Base1(); char a; };
struct Base2 { virtual ~Base2(); char b; };

struct Derived: Base1, Base2 {};

一个 Derived 的实例应该长这个样子(它是基于 gcc 的,因为它实际上取决于编译器...):
|      Cell 1       | Cell 2 |      Cell 3        | Cell 4 |
| vtable pointer    |    a   | vtable pointer     |    b   |
|         Base 1             |        Base 2               |
|                     Derived                              |

现在考虑转换所需的工作:

  • Derived 转换到 Base1 不需要任何额外的工作,它们位于相同的物理地址
  • Derived 转换到 Base2 需要将指针向右移动 2 个字节

因此,为了能够在一个派生对象和其基类之间进行强制类型转换,需要知道对象的内存布局。这只有编译器知道,该信息无法通过任何 API 访问,也没有标准化或其他什么东西。

在代码中,这将被翻译为:

Derived derived;
Base2* b2 = reinterpret_cast<Base2>(((char*)&derived) + 2);

当然,这是用于 static_cast 的。
如果你能在 dynamic_cast 的实现中使用 static_cast,那么你可以利用编译器并让它为你处理指针算术运算...但你仍然没有摆脱困境。
dynamic_cast
首先,我们需要澄清 dynamic_cast 的规范:
  • dynamic_cast<Derived*>(&base); 如果 base 不是 Derived 的一个实例,则返回 null。
  • dynamic_cast<Derived&>(base); 在这种情况下抛出 std::bad_cast 异常。
  • dynamic_cast<void*>(base); 返回最派生类的地址
  • dynamic_cast 遵守访问规范(publicprotectedprivate 继承)
我不知道你是否和我一样,但我认为这将会很丑陋。在这里使用 typeid 是不够的:
struct Base { virtual ~Base(); };
struct Intermediate: Base {};
struct Derived: Base {};

void func()
{
  Derived derived;
  Base& base = derived;
  Intermediate& inter = dynamic_cast<Intermediate&>(base); // arg
}

这里的问题在于typeid(base) == typeid(Derived) != typeid(Intermediate),因此您也不能依靠它。

还有一件有趣的事情:

struct Base { virtual ~Base(); };
struct Derived: virtual Base {};

void func(Base& base)
{
  Derived& derived = static_cast<Derived&>(base); // Fails
}

static_cast在涉及虚继承时无法使用...所以我们面临指针算术计算的问题。

一个几乎解决方案

class Object
{
public:
  Object(): mMostDerived(0) {}
  virtual ~Object() {}

  void* GetMostDerived() const { return mMostDerived; }

  template <class T>
  T* dynamiccast()
  {
    Object const& me = *this;
    return const_cast<T*>(me.dynamiccast());
  }

  template <class T>
  T const* dynamiccast() const
  {
    char const* name = typeid(T).name();
    derived_t::const_iterator it = mDeriveds.find(name);
    if (it == mDeriveds.end()) { return 0; }
    else { return reinterpret_cast<T const*>(it->second); }
  }

protected:
  template <class T>
  void add(T* t)
  {
    void* address = t;
    mDerived[typeid(t).name()] = address;
    if (mMostDerived == 0 || mMostDerived > address) { mMostDerived= address; }
  }

private:
  typedef std::map < char const*, void* > derived_t;
  void* mMostDerived;
  derived_t mDeriveds;
};

// Purposely no doing anything to help swapping...

template <class T>
T* dynamiccast(Object* o) { return o ? o->dynamiccast<T>() : 0; }

template <class T>
T const* dynamiccast(Object const* o) { return o ? o->dynamiccast<T>() : 0; }

template <class T>
T& dynamiccast(Object& o)
{
  if (T* t = o.dynamiccast<T>()) { return t; }
  else { throw std::bad_cast(); }
}

template <class T>
T const& dynamiccast(Object const& o)
{
  if (T const* t = o.dynamiccast<T>()) { return t; }
  else { throw std::bad_cast(); }
}

在构造函数中,你需要一些小东西:

class Base: public Object
{
public:
  Base() { this->add(this); }
};

那么,让我们来看看:

  • 经典用法:没问题
  • virtual继承?应该可以运行...但未经测试
  • 尊重访问权限...好棘手啊 :/

祝愿任何试图在编译器之外实现这个的人好运 :x


1
请在您顶部的类布局的图形描述中更改“Byte”为其他内容,因为vtable指针[几乎肯定]不是一个Byte。 - iAdjunct
1
@iAdjunct:确实,我使用了Cell - Matthieu M.

1
一种方法是声明一个静态标识符(例如一个整数),用来定义类的ID。在类中,您可以实现静态和作用域例程,这些例程返回类的标识符(请记住将例程标记为虚拟的)。
静态标识符应在应用程序初始化时进行初始化。一种方法是为每个类调用一个InitializeId例程,但这意味着必须知道类名,并且每次修改类层次结构时都必须修改初始化代码。另一种方法是在构造时检查有效的标识符,但这会引入额外的开销,因为每次构造一个类时都会执行检查,但只有第一次是有用的(此外,如果没有构造任何类,则静态例程无法使用,因为标识符从未初始化)。
一个合理的实现可能是一个模板类:
template <typename T>
class ClassId<T>
{
    public:

    static int GetClassId() { return (sClassId); }

    virtual int GetClassId() const { return (sClassId); }

    template<typename U> static void StateDerivation() {
        gClassMap[ClassId<T>::GetClassId()].push_back(ClassId<U>::GetClassId());
    }

    template<typename U> const U DynamicCast() const {
        std::map<int, std::list<int>>::const_iterator it = gClassMap.find(ClassId<T>); // Base class type, with relative derivations declared with StateDerivation()
        int id = ClassId<U>::GetClassId();

        if (id == ClassId<T>::GetClassId()) return (static_cast<U>(this));

        while (it != gClassMap.end()) {
            for (std::list<int>::const_iterator = pit->second.begin(), pite = it->second->end(); pit != pite; pit++) {
                if ((*pit) == id) return (static_cast<U>(this));
                // ... For each derived element, iterate over the stated derivations.
                // Easy to implement with a recursive function, better if using a std::stack to avoid recursion.
            }
        }

        return (null); 
    }  

    private:

    static int sClassId;
}

#define CLASS_IMP(klass) static int ClassId<klass>::sClassId = gClassId++;

// Global scope variables    

static int gClassId = 0;
static std::map<int, std::list<int>> gClassMap;

每个从ClassId派生的类都应该定义CLASS_IMP,gClassId和gClassMap应该在全局范围内可见。

所有类都可以访问单个静态整数变量保留的可用类标识符(基类ClassID或全局变量),每当分配新的类标识符时,该变量就会递增。

为了表示类层次结构,只需要一个将类标识符与其派生类之间映射的映射。要知道任何类是否可以转换为特定类,请遍历映射并检查声明的派生。

有许多困难要面对...使用引用!虚拟派生!错误的转换!错误的类类型映射初始化将导致转换错误...


类之间的关系应该手动定义,并在初始化例程中硬编码。这样可以确定一个类是否派生自另一个类,或者两个类是否有共同的派生。

class Base : ClassId<Base> {  }
#define CLASS_IMP(Base);
class Derived : public Base, public ClassId<Derived> {  }
#define CLASS_IMP(Derived);
class DerivedDerived : public Derived, public ClassId<DerivedDerived> {  }
#define CLASS_IMP(DerivedDerived);

static void DeclareDerivations()
{
    ClassId<Base>::StateDerivation<Derived>();
    ClassId<Derived>::StateDerivation<DerivedDerived>();
}

就我个人而言,我同意“编译器实现dynamic_cast是有原因的”的说法;可能是因为编译器做得更好(特别是在示例代码方面!)。


假设你发现你引用了出现在同一继承层次结构中的类,你打算如何实际执行 dynamic_cast - CB Bailey
更新了。我觉得解决方案路径很明显。 - Luca
你似乎在使用reinterpret_castClassId特化的结果作为DynamicCast的结果。毫无疑问,这是错误的类别转换;即使它是正确的类别,reinterpret_cast也不会对MI层次结构进行适当的调整。我是否在你的解决方案中漏掉了一些明显的东西? - CB Bailey
我不确定我是否清楚地突出了我的问题。static_cast 也不正确。考虑 struct A { virtual ~A(); }; struct B : virtual A {}; struct C : virtual A {}; struct D : B, C {}; 即使 BC 可能是同一个 D 实例的子对象指针,它们之间的 static_cast 是无效的,而且 reinterpret_cast 极不可能产生正确的结果。只有 dynamic_cast 可以保证产生正确的结果,但这就是我们想要复制的。 - CB Bailey

0

为了尝试一个略微不那么常规但又略微不那么明确的答案:

你需要做的是将指针转换为int*,在堆栈上创建一个新类型T,将其指针转换为int*,并比较两种类型中的第一个int。这将进行vtable地址比较。如果它们是相同类型,则它们将具有相同的vtable。否则,它们不会。

我们中更明智的人只需在我们的类中插入一个整数常量即可。


但这并不告诉您是否可以在两种类型之间进行dynamic_cast,以及如何执行转换。 - CB Bailey

-1
简单。从某个带有虚函数WhoAmI()的typeid接口派生所有对象。 在所有派生类中重写它。

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