静态转换接口类到内部引擎实现

static_cast interface class to internal engine implementation

本文关键字:内部 引擎 实现 转换 接口 静态      更新时间:2023-10-16

我正在开发一个3D引擎,假设我有以下接口类:

class IA {
public:
    virtual ~IA() {}
    virtual void doSomething() =0;   
};
class IB {
public:
    virtual ~IB() {}
    virtual void bindA( IA* ) =0;
};

如果你想获得类型为"IA"或"IB"的对象,你必须从依赖于正在使用的后端API(例如OpenGL)的工厂获得它们。函数IB::bindA(IA*)需要访问IA实现中的数据,为了实现这一点,它对实现类做了一个static_cast,然后直接访问它的元素。

我想知道你对static_cast的这种特殊使用有什么看法,你认为它是糟糕的设计吗?还是你觉得可以?

无论使用什么后端API,引擎都必须提供相同的接口,所以我认为我不能使用虚拟函数来实现这一点,因为我无法事先知道IB从IA需要什么。

谢谢:D

编辑

问题是引擎有以下两个类:

class IHardwareBuffer {
public:
    virtual ~IHardwareBuffer() {}
    virtual void allocate( .... ) =0;
    virtual void upload( .... ) =0;
};

class IMesh {
public:
    virtual ~IMesh() {}
    virtual bindBuffer( IHardwareBuffer* ) =0;
    ...
};

我"可以"合并IMesh和IHardwareBuffer类在一起,但这不会有太大的意义,因为HardwareBuffer只是一个"哑"的内存与顶点数据在它,和一个网格是一个或两个HardwareBuffers与其他数据周围,如顶点格式,材料等。让它们成为单独的类允许客户端代码有几个网格共享一个共同的HardwareBuffer和类似的东西。

在我看来,从设计的角度来看,这实际上是一个非常糟糕的主意。

如果您使用接口(或模拟接口,因为c++没有这样的语言结构),则使用它们来发布这些数据,这些数据在其他地方需要。因此,如果一个实现IB的对象必须将IA转换为某些东西来检索其数据,这显然是一个迹象,要么IA发布的数据不够,要么实现IA的对象也应该实现另一个更宽的接口。

很难说哪个选项更好(或者是否有其他选项),因为我们不知道这里的上下文。一般来说,如果真的没有必要,应该避免使用强制类型转换,而且这里显然是没有必要。


编辑:

The engine has to provide the same interface no matter what backend API is being used, so I don't think I could achieve this using virtual functions because I can't know beforehand what is needed by IB from IA. -这是一个糟糕的设计。

引擎应该以这样的方式编写,即它完全独立于使用它的实现,反之亦然。这就是使用接口、基类和多态性的全部要点:你应该能够编写另一个引擎,将它与现有的引擎交换,并且在实现中不做任何改变的情况下,一切都应该工作。


编辑(回复评论):

我认为,更明确的解决方案是转换到另一个接口,而不是具体实现,即:

class A : public IA, public IInternalA
{
     // Implementation
};
// Inside B:
void B::Process(IA * a)
{
    IInternalA ia = dynamic_cast<IInternalA *>(a);
    if (ia != nullptr)
        // Do something
}

这样你仍然可以从实现中分离出来(例如,你可以把它分成两个独立的部分),但是在你的引擎中,所有的类将足够了解彼此以正常工作。

对象有动态类型和静态类型,前者可以在运行时从它的虚函数表中读取,后者可以在源代码中声明。

要基于动态类型安全地强制转换,请使用dynamic_cast。如果在不查看虚函数表的情况下已经知道了动态类型,那么可以将dynamic_cast优化为static_cast。这可能会有意义地提高性能,这样做并没有错,只要它是有效的。

但是,过于频繁地转换为派生类引用的代码可能存在关注点分离问题。类层次结构的意义在于泛化。

我建议使用引用,而不是指针,因为如果强制转换无效,dynamic_cast的引用形式会抛出异常。然后你可以这样做:

// Check dynamic type (and throw exception) for debug build only
#ifndef NDEBUG
#define downcast static_cast
#else
#define downcast dynamic_cast
#endif
Iopengl &glenv = downcast< Iopengl & >( myIA );

如果总是知道实际的动态类型而不去vtable(例如全局opengl标志),那么vtable当然是多余的。你可以用标志和分支来代替虚拟调度来编写整个程序。

无论使用什么后端API,引擎都必须提供相同的接口,所以我认为我不能使用虚拟函数来实现这一点,因为我无法事先知道IB从IA需要什么。

如前所述,抽象基类提供接口,派生类调用后端。你的例子有点粗略,但看起来IA和IB是接口,如果你要达到你的目标,你必须定义一个独立于后端的方式,无论实现。

现在我想我明白你的问题了。你有几个几乎没有共同点的后端,你需要在一个隐藏它们差异的引擎中使用它们。

现在的问题是,如果两个类型没有共同之处,它们不应该从一个共同的基继承。当然,像将指针存储在void*中这样的黑客只是在地毯下扫灰尘。所以我们不要那样做

因此需要为每个后端提供包装器。所有包装器都应该遵循相同的接口,但就其实现而言没有任何共同之处。工厂应该返回一个包装器。

class IBackendWrapper
{
  public:
   ... backend pure virtual functions ...
};
class OpenGLBackendWrapper : public IBackendWrapper
{
  public:
   ... backend virtual function immplementations in terms of OpenGL ...
  private:
    ... OpenGL data ...
};
class X11BackendWrapper : public IBackendWrapper
{
  public:
   ... backend virtual function immplementations in terms of X11...
  private:
    ... X11 data ...
};
class BackendFactory
{
  public:
    IBackendWrapper* getbackend();
};

现在你的引擎可以使用IBackendWrapper,而不用担心具体的后端。

如果你的3D抽象很肤浅,那么每个包装器都可能是你的整个引擎。然后,引擎类将退化为一个简单的转发器。这没问题。