C++ 使用抽象工厂和模板元编程的可扩展体系结构

C++ 使用抽象工厂和模板元编程的可扩展体系结构,c++,oop,architecture,C++,Oop,Architecture,我目前正在写我的硕士论文,似乎我找不到一个令人满意的解决以下问题的方法。这里的想法是设计一个从底层API(如DirectX11和OpenGL4)提取的小型库。在同一个应用程序中,不需要两个或多个API共存,因此理论上我可以编写一组预处理器指令来区分它们,但是这会破坏我的代码,当然,它根本不可扩展 抽象工厂似乎非常方便,但是我似乎找不到一种方法使它与模板一起工作 让我们开始 我有一个抽象类工厂,其目的是实例化应用程序工作所需的对象,例如资源和上下文。前者用于在运行时加载资源,而后者用于渲染三维场景

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

抽象工厂似乎非常方便,但是我似乎找不到一种方法使它与模板一起工作

让我们开始

我有一个抽象类
工厂
,其目的是实例化应用程序工作所需的对象,例如
资源
上下文
。前者用于在运行时加载资源,而后者用于渲染三维场景。
资源
上下文
都是抽象的,因为它们的实现依赖于底层API

class Factory{
   public:
      virtual Resources & GetResources() = 0;
      virtual Context & GetContext() = 0;
}
Resources
类将加载所需的资源,并返回类型为
Texture2D
Mesh
的对象。同样,这些类是抽象的,因为它们依赖于特定的API

假设我正在使用DirectX11和OpenGL4.5。对于这些API中的每一个,我都派生了上面的类,分别是
DX11Factory
DX11Resources
DX11Context
DX11Texture2D
DX11Mesh
等等。他们扩展的类是非常明显的。很公平

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

class Resources{
   public:
      Texture2D LoadTexture(const wstring & path) = 0;
      Mesh LoadMesh(const wstring & path) = 0;
}
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 );

         }
   }

}
DX11Resource
将实现上述方法,一切都将正常工作。。。除了如果我想在将来支持一种新的资源类型,比如
TextureCube
(从软件工程师的角度来看,我现在肯定会支持),我必须在库用户实际使用的界面中声明一种新方法
TextureCube LoadTextureCube(…)
,即
Resources
。这意味着我必须在每个派生类中实现该方法(开-闭原则FTW!)

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

class Resources{
   public:
      Texture2D LoadTexture(const wstring & path) = 0;
      Mesh LoadMesh(const wstring & path) = 0;
}
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 );

         }
   }

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

我曾考虑过其他解决方案,但没有一个能满足我的需求:

1. 我可以用这个:

template <typename TDerived>
class Resources{
   public:

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

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

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

3.访客 利用访客模式。这种方法实际上对我一点帮助都没有,但我把它放在这里以防万一(每当我看到一个无休止的if-then-else与集成的downcast级联时,我总是想到访问者,就像前面的一点:)

模板资源特性;
模板资源{
使用visitable=Texture2DVisitable;
}
结构纹理2dvisitable{
Texture2D运算符()(常量wstring和path、加载器和访问者){
return visitor.Load(路径,*this);
}
}
模板
TResource资源::加载(路径){
返回typename资源特性::visitable()(路径,*this);
}
使用这种方法,
Resources
现在必须为它可以加载的每个资源声明一个纯虚拟方法,比如
Texture2D Resources::load(path,Texture2DVisitable&)=0
。所以,再一次,在新资源的情况下,我必须相应地更新整个层次结构。。。在这一点上,我将在开始时使用平凡的解决方案

4.其他人? 我错过什么了吗?我应该选择什么方法?我觉得我总是在把事情复杂化

提前谢谢你,很抱歉我写得很差


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

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

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

至于丑陋,好吧,这是你可以通过一些技巧来改进的东西,但是它总是会有一些丑陋,尤其是
静态\u cast
,它不能被删除,因为你需要一种从运行时调度返回静态类型结果的方法e> 标准::类型索引:

// 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);
};
//Resources.h:
班级资源{
公众:
模板
TResource负载(常数wstring和路径){
return*static_cast(加载)路径,std::type_索引(typei
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.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);
};