使用抽象工厂和模板元编程的可扩展的建筑学

时间:2014-08-18 18:18:57

标签: c++ oop architecture

我正在研究我的硕士论文,似乎我找不到令人满意的解决方案来解决以下问题。这里的想法是设计一个应该从底层API(例如DirectX11和OpenGL4)中抽象出来的小型库。两个或多个API不需要在同一个应用程序中共存,因此理论上我可以编写一堆预处理程序指令来区分它们,但是这会破坏我的代码,当然,它根本不可扩展。 / p>

抽象工厂似乎非常方便,但似乎我无法找到一种方法使其适用于模板。

让我们开始......

我有一个抽象类Factory,其目的是实例化应用程序工作所需的对象,例如ResourcesContext。前者用于在运行时加载资源,而后者用于渲染3D场景。 ResourcesContext都是抽象的,因为它们的实现取决于底层API。

class Factory{
   public:
      virtual Resources & GetResources() = 0;
      virtual Context & GetContext() = 0;
}

Resources类将加载所需的资源,并返回Texture2DMesh类型的对象。同样,这些类是抽象的,因为它们依赖于特定的API。

假设我正在使用DirectX11和OpenGL4.5。对于每个API,我都有上面派生的类,分别是DX11FactoryDX11ResourcesDX11ContextDX11Texture2DDX11Mesh等等。他们扩展的课程非常明显。很公平。

设计类Resource的接口的简单方法如下:

class Resources{
   public:
      Texture2D LoadTexture(const wstring & path) = 0;
      Mesh LoadMesh(const wstring & path) = 0;
}

DX11Resource将实现上述方法,一切都会正常工作......除非我想在将来支持新的资源类型,如TextureCube(以及软件工程师)我的观点是肯定的。现在I don't care),我将不得不在库用户实际使用的界面中声明一个新方法TextureCube LoadTextureCube(...),即Resources。这意味着我必须在每个单个派生类中实现该方法(开放闭合原则FTW!)。

我解决这个问题的第一个想法是:

class Texture2D{...}

class Resources{
   public:
      template<typename TResource>
      virtual TResource Load(const wstring & path) = 0; // :(
}   

namespace dx11{

   class DX11Texture2D: public Texture2D{...}
   class DX11Texture2DLoader{...}

   template<typename TResource> struct resource_traits;

   template<> struct resource_traits<Texture2D>{

      using type = DX11Texture2D;
      using loader = DX11Texture2DLoader; //Functor type

   }

   class DX11Resources{
      public:
         template<typename TResource>
         virtual TResource Load(const wstring & path){

            return typename resource_traits<TResource>::loader()( path );

         }
   }

}

因此,如果我需要支持一种新类型的资源,我可以简单地在适当的命名空间内声明一个新的resource_traits(当然还有新的资源摘要和具体类型),一切都会起作用。不幸的是,不支持虚拟模板方法(并且出于一个很好的理由,想象一下写这样的东西会发生什么

Resources * r = GrabResources(); //It will return a DirectX9 object
r->Load<HullShader>(L"blah");  //DX9 doesn't support HullShaders, thus have no resource_traits<HullShader>

所以基本上编译器将无法执行正确的替换,并且它会向用户甚至不知道的类指出错误。 )

我已经考虑过其他解决方案,但它们都不能满足我的需求:

1。 CRTP

我可以用这个:

template <typename TDerived>
class Resources{
   public:

      template <typename TResource>
      TResource Load(const wstring & path){

         return typename TDerived::resource_traits<TResource>::loader()( path );

      }
}

我认为这样可行,但Resources<TDerived>对象无法返回Factory只是因为TDerived未知(最终程序员不应该这样)。

2。 RTTI

class Resources{
   template <typename TResource>
   TResource Load(const wstring & path){

      return *static_cast<TResource *>( Load(path, typeid(TResource).hash_code()) );

   }

   virtual void * Load(const wstring & path, size_t hash) = 0;
}

在派生类中,我必须实现上面的纯虚方法,然后使用if-then-else级联我可以实例化我需要的资源,或者如果特定API不支持它,则返回nullptr。这肯定会起作用,但是丑陋当然,只要我想支持一种新的资源类型,它就会强制我重写实现(但至少它只是一个类)!

if( hash == typeid(Texture2D).hash_code()) // instantiate a DX11Texture2D
else if (...)...

3。访问者

利用访客模式。这个方法根本不会对我有所帮助,但我把它留在这里以防万一(每当我看到一个永无止境的if-then-else级联与集成的downcast时,我总是想到访问者,就像在前一点:) )。

template <typename TResource> resource_traits;

template<> resource_traits<Texture2D>{

   using visitable = Texture2DVisitable;

}

struct Texture2DVisitable{

   Texture2D operator()(const wstring & path, Loader & visitor){

      return visitor.Load(path, *this);

   }

}

template<typename TResource>
TResource Resources::Load(path){

   return typename resource_traits<TResource>::visitable()(path, *this);

}

使用这种方法Resources现在必须为它可以加载的每个资源声明一个纯虚方法,如Texture2D Resources::Load(path, Texture2DVisitable &) = 0。所以,再次,在新资源的情况下,我必须相应地更新整个层次结构......此时我将在开头使用简单的解决方案。

4。其他

我错过了什么吗?我应该选择哪种方法?我觉得我一如既往地过于复杂化了!

提前致谢并抱歉我写得不好的墙上文字!

ps:首先摆脱Resource类不是一个选项,因为它的真正目的是防止反复加载相同的资源。它基本上是一个巨大的轻量级。

1 个答案:

答案 0 :(得分:1)

这个问题实际上归结为整个“虚函数模板”问题。基本上,解决方案(无论它是什么)必须采用编译时信息(例如,模板参数),将其转换为运行时信息(例如,值,类型ID,哈希码,函数指针等)。 ),经过运行时调度(虚拟调用),然后将运行时信息转换回编译时信息(例如,要执行的代码段)。通过了解这一点,您将意识到最直接的解决方案是使用“RTTI”解决方案或其变体。

正如您所指出的,该解决方案唯一真正的问题是它“丑陋”。我同意它有点难看,除此之外,它是一个很好的解决方案,尤其是在添加新的受支持类型时所需的修改仅本地化到与要添加的类关联的实现(cpp文件)支持(你真的不希望有更好的东西)。

至于丑陋,嗯,这是你可以随时用一些诡计改进的东西,但总会有一些丑陋,特别是static_cast无法删除,因为你需要一种方法来摆脱运行时调度回到静态类型的结果。这是一个可能的解决方案,它依赖于std::type_index

// Resources.h:

class Resources {
  public:
    template <typename TResource>
    TResource Load(const wstring & path){
      return *static_cast<TResource *>(Load(path, std::type_index(typeid(TResource))));
    }

  protected:
    virtual void* Load(const wstring & path, std::type_index t_id) = 0;
}

// DX11Resources.h:

class DX11Resources : public Resources {
  protected:
    void* Load(const wstring & path, std::type_index t_id);
};

// DX11Resources.cpp:

template <typename TResource>
void* DX11Res_Load(DX11Resources& res, const wstring & path) { };

template <>
void* DX11Res_Load<Texture2D>(DX11Resources& res, const wstring & path) {
  // code to load Texture2D
};

// .. so on for other things..

void* DX11Resources::Load(const wstring & path, std::type_index t_id) {
  typedef void* (*p_load_func)(DX11Resources&, const wstring&);
  typedef std::unordered_map<std::type_index, p_load_func> MapType;

  #define DX11RES_SUPPORT_LOADER(TYPENAME) MapType::value_type(std::type_index(typeid(TYPENAME)), DX11Res_Load<TYPENAME>)

  static MapType func_map = {
    DX11RES_SUPPORT_LOADER(Texture2D),
    DX11RES_SUPPORT_LOADER(Texture3D),
    DX11RES_SUPPORT_LOADER(TextureCube),
    //...
  };

  #undef DX11RES_SUPPORT_LOADER

  auto it = func_map.find(t_id);
  if(it == func_map.end())
    return nullptr;  // or throw exception, whatever you prefer.

  return it->second(*this, path);
};

这有一些变化(比如有成员函数而不是加载器的自由函数,或使用非模板函数而不是专门化,或者这两种修改),但基本的想法是添加一个新的支持的类型,您只需将其添加到列表中支持的类型列表(DX11RES_SUPPORT_LOADER(SomeType)),并将代码创建为新函数(仅在cpp文件中)。那里仍然有一些丑陋,但是头文件是干净的,虚拟“加载”中的丑陋是复杂的“O(1)”,这意味着你不会为每个新类型添加丑陋,它是恒定代码的丑陋代码(而不是if-else序列,其中丑陋代码的数量与支持的类型数量成比例)。此外,这样做的另一个好处是可以更快地进行调度(使用哈希表)。此外,使用type_index对于避免与两种类型的哈希值冲突很重要(您不会丢失有关使用哪个typeid创建哈希值的信息)。

所以,总而言之,我的建议是采用“RTTI”解决方案,并尽你所能或想要消除与之相关的一些丑陋或低效率。最重要的是保持派生类的接口(头部,类声明)尽可能干净,以避免将来向它添加任何东西(你肯定不希望该类在其声明中公开)它通过函数声明或其他东西支持哪些类型的资源,否则,每次都要重新编译世界以添加一个。)

N.B。:如果你需要避免使用RTTI(例如,-fno-rtti选项),那么有办法解决这个问题,但这超出了这个问题的范围。