我一直在我的渲染引擎中做类似的事情。我有一个模板化的 IResource 接口类,从中继承了各种资源(为简洁起见):
template <typename TResource, typename TParams, typename TKey>
class IResource
{
public:
virtual TKey GetKey() const = 0;
protected:
static shared_ptr<TResource> Create(const TParams& params)
{
return ResourceManager::GetInstance().Load(params);
}
virtual Status Initialize(const TParams& params, const TKey key, shared_ptr<Viewer> pViewer) = 0;
};
Create 静态函数回调一个模板化的 ResourceManager 类,该类负责加载、卸载和存储它使用唯一键管理的资源类型的实例,确保从存储中简单地检索重复调用,而不是重新加载为单独的资源。
template <typename TResource, typename TParams, typename TKey>
class TResourceManager
{
sptr<TResource> Load(const TParams& params) { ... }
};
具体资源类利用 CRTP 从 IResource 继承。专门针对每种资源类型的 ResourceManager 被声明为这些类的朋友,因此 ResourceManager 的 Load 函数可以调用具体资源的 Initialize 函数。一个这样的资源是纹理类,它进一步使用 pImpl 习惯用法来隐藏其私有:
class Texture2D : public IResource<Texture2D , Params::Texture2D , Key::Texture2D >
{
typedef TResourceManager<Texture2D , Params::Texture2D , Key::Texture2D > ResourceManager;
friend class ResourceManager;
public:
virtual Key::Texture2D GetKey() const override final;
void GetWidth() const;
private:
virtual Status Initialize(const Params::Texture2D & params, const Key::Texture2D key, shared_ptr<Texture2D > pTexture) override final;
struct Impl;
unique_ptr<Impl> m;
};
我们的纹理类的大部分实现是独立于平台的(例如 GetWidth 函数,如果它只返回一个存储在 Impl 中的 int)。但是,根据我们所针对的图形 API(例如 Direct3D11 与 OpenGL 4.3),某些实现细节可能会有所不同。一种解决方案可能是从 IResource 继承一个中间 Texture2D 类,该类定义所有纹理的扩展公共接口,然后从中继承一个 D3DTexture2D 和 OGLTexture2D 类。此解决方案的第一个问题是它要求您的 API 用户不断注意他们所针对的图形 API(他们可以在两个子类上调用 Create)。这可以通过将Create 限制为中间Texture2D 类来解决,该类可能使用#ifdef 开关来创建D3D 或OGL 子对象。但是这个解决方案仍然存在第二个问题,即平台无关的代码将在两个孩子之间重复,从而导致额外的维护工作。您可以尝试通过将独立于平台的代码移动到中间类来解决这个问题,但是如果某些成员数据同时被特定于平台的代码和独立于平台的代码使用,会发生什么情况呢? D3D/OGL 子级将无法访问中介 Impl 中的这些数据成员,因此您必须将它们移出 Impl 并移入标头,以及它们携带的任何依赖项,从而暴露任何包含您的标头的人所有他们不需要知道的废话。
API 应该易于正确使用而难以错误使用。易于使用的部分权利是限制用户只接触他们应该使用的 API 部分。该解决方案使其容易被错误使用并增加了维护开销。用户应该只关心他们在一个地方定位的图形 API,而不是他们使用您的 API 的任何地方,并且他们不应该暴露于您的内部依赖项。这种情况需要部分类,但它们在 C++ 中不可用。因此,您可以简单地在单独的头文件中定义 Impl 结构,一个用于 D3D,一个用于 OGL,并在 Texture2D.cpp 文件的顶部放置一个#ifdef 开关,然后通用定义公共接口的其余部分.这样,公共接口可以访问它需要的私有数据,唯一重复的代码是数据成员声明(构造仍然可以在创建 Impl 的 Texture2D 构造函数中完成),您的私有依赖项保持私有,而用户不需要除了在暴露的 API 表面中使用有限的调用集之外,必须关心任何事情:
// D3DTexture2DImpl.h
#include "Texture2D.h"
struct Texture2D::Impl
{
/* insert D3D-specific stuff here */
};
// OGLTexture2DImpl.h
#include "Texture2D.h"
struct Texture2D::Impl
{
/* insert OGL-specific stuff here */
};
// Texture2D.cpp
#include "Texture2D.h"
#ifdef USING_D3D
#include "D3DTexture2DImpl.h"
#else
#include "OGLTexture2DImpl.h"
#endif
Key::Texture2D Texture2D::GetKey() const
{
return m->key;
}
// etc...