【问题标题】:where downcasting is actually useful?向下转换实际上有用吗?
【发布时间】:2020-10-27 20:28:24
【问题描述】:

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

【问题讨论】:

  • 我一直发现使用dynamic_cast 表示存在设计缺陷。 (即使我在自己的代码中使用过它。Bad Eljay,没有饼干。)可能有有效的用例,但我还没有遇到过。
  • 可能会用到这个,通常在复杂的框架中,其他设计考虑可能会阻止使用真正的虚拟接口或通过去虚拟化提供切实的性能优势。
  • static_cast 也可以执行向下转型。

标签: c++ oop polymorphism downcast


【解决方案1】:

奇怪的重复模板模式 (CRTP)

向下转换实际上在哪里有用?

在实现Curiously recurring template pattern时非常有用:

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】:
    1. 您从不太信任的外部/用户代码中获取抽象类。您需要验证它是否与您期望的类型匹配。如果它不匹配你报告错误而不是去UB。此外,如果您将过多的代码/类暴露给用户/外部代码,这将是一个问题——因此您将它们隐藏在接口类后面。

    2. 对于通用对象管理器类很有用——它存储抽象类而不了解它们是什么。因此,每当用户尝试获取其中一个并转换为适当的类型时 - 它应该应用动态转换以确保用户不会弄乱这些类型。

    3. 在处理具有非平凡层次结构的复杂类时需要它——简单的指针转换可能会失败。不过,一般不建议处理此类类。

    【讨论】:

      【解决方案3】:

      我在 .NET 和 Java 等面向对象框架中看到向下转换有用的主要情况是,所有实现某个接口的对象都可以以某种方式执行某些任务,但有些也可以用于以更快的方式执行任务。例如,一个人可能有某种类型的实例,它表现为一个可以按索引读取的集合,并且希望将该类型中的一系列项目复制到项目类型的数组中。如果源对象恰好是公开包装数组的已知数组包装类型的实例(我认为 C++ Vector&lt;T&gt; 会这样做),则向下转换为该类型,访问数组并从中复制元素可能比通过包装器访问每个单独的元素要快得多。

      在我见过的大多数此类情况下,如果基接口包含更多方法,这些方法的行为以可以由所有类型实现的方式指定,即使不是所有实现都可以有用。例如,“按索引读取”集合可以包含一个函数,该函数将返回一个结构,该结构包含数组引用、偏移量和可用下标范围(如果它包装了可访问的后备数组),或者空数组偏移量(如果后备阵列不可访问)。至少在 .NET 中,我认为在 Java 中(不确定 .NET),调用已知对象支持的接口方法比测试对象是否支持接口要快。我怀疑 Java 和 .NET 在其许多广泛使用的接口(如 Enumerable&lt;T&gt; (Java) 或 IEnumerable&lt;T&gt; (.NET))中没有包含此类功能的原因是它们一开始并没有对默认设置提供任何支持接口方法,因此包含此类方法会大大增加常见实现中的膨胀量。

      【讨论】:

        【解决方案4】:

        没有使用动态向下转换的常见习语/模式,因为它不是很有用。向下转换的使用表明设计不佳。

        如果您发现自己处于一种罕见的情况,您认为需要动态向下转换,因为您已经被框架或库的设计画到了角落,那么请知道动态转换适合您。但大多数时候(希望如此),你不会处于那种情况。

        如果你想不出需要向下转换的情况,那么你就在一个好地方,并且有大多数程序员陪伴。

        对于静态向下转换,请参阅 dfri 的回答。

        【讨论】:

        • @dfri 好点。我只是在回答中考虑动态向下转换。静态向下转换在 CRTP 中确实很有用。
        【解决方案5】:

        向下投射在虚幻引擎中被广泛使用。甚至还有一个专用的Cast 函数,它对UObject 派生类型的整数表示进行操作,这使得它在性能方面很便宜。

        引擎带有一个基本类型的层次结构,应该在您的游戏模块中继承和扩展。问题是这些类型拥有指向自身的基类型指针,因此当您扩展这些类型时,您仍将使用由基本引擎类型定义的变量,除非您定义自己的变量——但这不会为您提供完全支持。

        如下代码在虚幻引擎游戏代码库中很常见。

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

        这并不是说它是良好架构的表现,而是肯定是实践的真实例子。

        【讨论】:

          【解决方案6】:

          通常有 API 或库使用回调来通知您事件。这些通常会有一个您传递的标签,该标签会被传递回回调函数。

          这种系统的问题是产生可以与代码对象相关联的唯一标签。最简单的系统是简单地将指向对象的指针转换为标记类型。如果在对象被销毁后出现回调,那将非常失败。

          我有一个替代方案,一个 mixin 类,它保存一个标签号和对象指针表。构造函数将生成一个唯一的标记 ID,并将其与指向对象的指针一起放在一个表中。析构函数会将其从表中移除。但由于存储在表中的指针是 mixin 类而不是实际对象,因此您需要进行强制转换以使其再次有用。

          template<typename T>
          T* TagMixin::ObjectFromTag(Tag tag)
          {
              TagMixin * p = TableLookup(tag);
              if (p == nullptr)  // (not found)
                  return nullptr;
              return dynamic_cast<T*>(p);
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-03-16
            • 1970-01-01
            • 1970-01-01
            • 2016-07-29
            相关资源
            最近更新 更多