向下转型在哪些情况下会有用处?

7

我知道downcasting基本上是将父类指针或引用转换为派生类引用或指针的转换,为此您需要使用dynamic_cast运算符。但我几乎想不出任何例子。你们能否解释一下?


6
我一直认为使用 dynamic_cast 表明设计存在缺陷(即使我自己的代码中使用了它,真是太糟糕了)。可能有一些合法的使用情况,但我还没有遇到过。 - Eljay
这可能有用途,通常在复杂的框架中,其他设计考虑可能会阻止使用真正的虚拟接口或通过去虚拟化提供有形的性能优势。 - SergeyA
1
static_cast 也可以执行向下转换。 - HolyBlackCat
6个回答

10

神奇的递归模板模式(CRTP)

向下转型在什么情况下实际上是有用的?

当实现神奇的递归模板模式(Curiously recurring template pattern,CRTP)时,向下转型非常有用:

template <class T> 
struct Base
{
    void interface()
    {
        // ...
        static_cast<T*>(this)->implementation();
        // ...
    }

    static void static_func()
    {
        // ...
        T::static_sub_func();
        // ...
    }
};

struct Derived : Base<Derived>
{
    void implementation();
    static void static_sub_func();
};
常见的基类模板接口提供了定义,将调用委托给派生类实现,对于非静态成员函数,通过将基类的this指针向下转换为相应派生类的指针类型(针对基类模板的特定专业化),然后分派到派生类函数来实现。请注意,在这种情况下,向下转换不一定需要使用动态转换来实现;在此示例中,使用静态转换将基类指针(this)转换为相应的派生类指针。

2
没有常见的成语/模式使用动态下转换,因为它没有太大的用处。使用下转换表明设计有问题。
如果您发现自己处于罕见的情况下,认为需要动态向下转换,因为您被框架或库的设计所限制,那么请知道,动态转换在那里等着您。但是大多数时候(希望如此),您不会处于这种情况。
如果您无法想出需要下转换的情况,则说明您处于一个好的位置,并且与大多数程序员一起。
对于静态下转换,请参见dfri的答案。

1
@dfri 很好的观点。我在我的回答中只考虑了动态向下转型。静态向下转型确实在CRTP中非常有用。 - eerorika

2
  1. 如果你从不太信任的外部/用户代码中获取抽象类,需要验证它是否与你期望的类型匹配。如果不匹配,应报告错误而不是进入未定义行为。此外,如果你将过多的代码/类暴露给用户/外部代码,这将是一个问题 - 因此你需要将它们隐藏在接口类后面。

  2. 对于通用对象管理器类非常有用 - 它存储了不理解其内容的抽象类。因此,每当用户尝试获取其中之一并转换为适当的类型时,应该进行动态转换以确保用户没有搞乱类型。

  3. 它需要处理具有非平凡层次结构的复杂类 - 对于这些类,简单的指针转换可能会失败。虽然一般情况下不建议处理这样的类。


2
在像.NET和Java这样的面向对象框架中,我看到downcasting实用的主要情况是,所有实现某个接口的对象都可以以某种方式执行某个任务,但有些对象也可以以更快的方式执行该任务。例如,可能会有一个某种类型的实例,它的行为类似于可按索引读取的集合,并且希望从该类型的某个范围内将项目复制到项目类型的数组中。如果源对象恰好是已知的包装数组类型的实例(我认为C++的Vector就是这样),则向下转换为该类型,访问数组并从中复制元素可能比通过包装器访问每个单独的元素快得多。
在我看到的大多数这样的情况中,如果基本接口包括更多方法,其行为以一种所有类型都可以实现的方式指定,即使不是所有实现都有用,downcasting的需求也可以避免。例如,可按索引读取的集合可以包括一个函数,该函数将返回一个结构,其中包含数组引用、偏移量和可用子脚本范围(如果它包装了可访问的支持数组),否则将返回空的数组偏移量(如果支持数组不可访问)。至少在.NET中,我想在Java中也是如此(不确定.NET),调用对象已知支持的接口方法要比测试对象是否支持接口快。我怀疑Java和.NET没有在许多广泛使用的接口中(如Enumerable(Java)或IEnumerable(.NET))包含这些功能的原因是它们没有从默认接口方法开始支持任何支持,因此包括这些方法将大大增加常见实现中的膨胀量。

1

在虚幻引擎中广泛使用向下转型。甚至有一个专门的Cast函数,用于操作UObject派生类型的整数表示,这使得它在性能方面非常便宜。

该引擎带有一组基础类型层次结构,应该在您的游戏模块中进行继承和扩展。问题是这些类型持有指向自身的基类型指针,因此当您扩展这些类型时,仍将使用基础引擎类型定义的变量,除非您定义自己的变量-尽管这不会给您完全的支持。

在虚幻引擎游戏代码库中,像下面的代码是常见的情况。

ACharacter* Character = GetCharacter(); // Base engine character type.
AMyCharacter* MyCharacter = Cast<AMyCharacter>(Character); // Extended game character type.

MyCharacter->Something();

这并不意味着它是良好架构的体现,但确实是实践的真实例证。

1
通常有些API或库使用回调来通知您事件。这些通常会有一个标签,您将其传递给回调函数,然后它会被传递回来。
这种系统的问题在于生成可以与代码对象关联的唯一标签。最简单的系统是将指向对象的指针直接转换为标签类型。如果对象被销毁后回调到来,这种方法将失败。
我有一个替代方案,即混合类,它保持标记号和对象指针的表格。构造函数将生成唯一的标记ID,并将其与对象指针一起放入表格中。析构函数将从表格中删除它。但由于存储在表格中的指针是混合类而不是实际对象,因此您需要进行转换才能再次使用它。
template<typename T>
T* TagMixin::ObjectFromTag(Tag tag)
{
    TagMixin * p = TableLookup(tag);
    if (p == nullptr)  // (not found)
        return nullptr;
    return dynamic_cast<T*>(p);
}

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