【问题标题】:Inheritance vs class template specialization. Design dude继承与类模板专业化。设计老兄
【发布时间】:2017-01-05 12:16:33
【问题描述】:

我有一个实现设计问题。我希望你能帮助我。假设我有以下课程

class A
{
public:
    vector<int> v() const { return m_v; }
    bool isValid() const { return m_v.size() > m_components; }
    int operator [] (const int index) const { return m_v[index]; }
    ...
private:
    vector<int> m_v;
    int m_components;
}

现在我希望m_v 向量可以是不同的类型,所以我可以模板类:

template<typename T>
class A
{
public:
    vector<T> v() const { return m_v; }
    T operator [] (const int index) const { return m_v[index]; }
    ...
private:
    vector<T> m_v;
    int m_components;
}

但是,我意识到当 T 类型为 double 时,我需要扩展类 A 并添加更多属性,例如另一个 vector&lt;bool&gt; m_foo; 并更改应使用这些新属性的几个方法。

这是我有疑问的地方。我想我有几个选择:

选项1:我可以创建一个非模板基类A,实现所有常用方法,并为每种不同类型派生多个类(具有自己的特定类属性和方法实现),即:Aint, Adouble, Afloat。此选项要求 vector&lt;...&gt; m_v; 存储在每个派生类中,因此我必须多次复制所有相同的代码才能访问每个派生类中的 m_v; 属性。例子中只有v()operator []isValid()这样的方法,但在实际问题中还有更多。

选项 2: 模板专业化。我可以为每种类型专门化类模板,因此只提供根据T 类型更改的特定方法的实现。但是,这会强制在模板类中存储大量内容,这些内容仅在 T 为特定类型时使用,即仅在 T 类型为 double 时使用的 m_foo 向量(在建议的例子)。因此,我在浪费内存。此外,实现模板类并为几乎大多数模板类型提供模板类专业化并存储仅用于特定类型的特定属性似乎不是很优雅甚至连贯。

我不知道我是否能很好地解释我的问题。希望如此。

提前谢谢你。 哈维尔。

【问题讨论】:

  • 那么,本质上,您希望 class A 具有不同的功能,具体取决于模板类型参数?
  • 是和不是。我有一些独立于T 类型共享的方法,但也取决于这种类型,我有一些具有特定实现甚至需要特定类变量的方法。
  • 您可以结合多种技术使其干燥。话虽这么说,这个问题太笼统了……这里没有什么可以作为我决定的依据。
  • "但是,我意识到当类型 T 为例如 double 时,我需要扩展类 A 并添加更多属性," 为什么?你想达到什么目标?对我来说,不清楚这里的用例是什么。看起来像一个XY问题!让我们谈谈不需要破坏的设计!
  • 你能举个例子吗?

标签: c++ templates inheritance


【解决方案1】:

我会尝试用一个与我的实际问题非常接近的例子来阐述这个问题,尽管它会变得更长。

选项1:考虑以下基于模板和模板特化的类实现。

template<typename T>
class A
{
public:
   A() {}
   vector<T> v() const { return m_v; }
   bool isValid() const  { return m_v.size() >= m_components; }
   T  operator [] (const int i) const { return m_v[i]; }
   T& operator [] (const int i)       { return m_v[i]; }
   int components() const { return m_components; }
   double value() const { return m_value; }
   void method1();
private:
   vector<T> m_v;
   int m_components;
   double m_value;
   vector<bool> m_indices;    // this is only used when T is int
   map<int, char> m_map;      // this is only used when T is double
   queue<int> m_queue;        // this is only used when T is bool
};

template<>
void A<int>::method1()
{
   for (int i = 0; i < m_components; ++i) m_v.push_back(i);
   // stuff only for int case
   for (int i = 0; i < m_components; ++i) { i % 2 == 0 ? m_indices[i] = true : m_indices[i] = false; }
}
template<>
void A<double>::method1()
{
   for (int i = 0; i < m_components; ++i) m_v.push_back(i);
   // stuff only for double case
   for (int i = 0; i < m_components; ++i) { i % 2 == 0 ? m_map[i] = 'e' : m_map[i] = 'o'; }
}
template<>
void A<bool>::method1()
{
   for (int i = 0; i < m_components; ++i) m_v.push_back(i);
   // stuff only for bool case
   for (int i = 0; i < m_components; ++i) { i % 2 == 0 ? m_queue.push(1) : m_queue.push(0); }
}

如您所见,无论T 类型如何,都有几种常用方法,并且涉及使用m_v 向量(方法:v()isValid()operator[])。但是,还有其他方法(method1())根据T类型有特定的实现,也需要根据这种类型使用特定的数据结构(queuesmapsvectors) .我在课堂上看到队列、地图、向量等的定义非常非常丑陋,尽管它们是否仅用于具体情况,具体取决于 T 类型。

选项 2: 另一种选择:

class A
{
public:
   A() {}
   int components() const { return m_components; }
   double value() const { return m_value; }
   virtual void method1() == 0;
protected:
   int m_components;
   double m_value;
};

/***** Derived A for int case ****/
class Aint : public A
{
public:
   Aint() {}
   vector<int> v() const { return m_v; }
   bool isValid() const  { return m_v.size() >= m_components; }
   int  operator [] (const int i) const { return m_v[i]; }
   int& operator [] (const int i)       { return m_v[i]; }

   void method1();
private:
   vector<int> m_v;
   vector<bool> m_indices;
};
void Aint::method1()
{
   for (int i = 0; i < m_components; ++i) m_v.push_back(i);
   for (int i = 0; i < m_components; ++i) { i % 2 == 0 ? m_indices[i] = true : m_indices[i] = false; }
}

/***** Derived A for double case ****/
class Adouble : public A
{
public:
   Adouble() {}
   vector<double> v() const { return m_v; }
   bool isValid() const  { return m_v.size() >= m_components; }
   double  operator [] (const int i) const { return m_v[i]; }
   double& operator [] (const int i)       { return m_v[i]; }

   void method1();
private:
   vector<double> m_v;
   map<int, char> m_map;
};
void Adouble::method1()
{
   for (int i = 0; i < m_components; ++i) m_v.push_back(i);
   for (int i = 0; i < m_components; ++i) { i % 2 == 0 ? m_map[i] = 'e' : m_map[i] = 'o'; }
}

/***** Derived A for bool case ****/
class Abool : public A
{
public:
   Abool() {}
   vector<bool> v() const { return m_v; }
   bool isValid() const  { return m_v.size() >= m_components; }
   bool  operator [] (const int i) const { return m_v[i]; }
   bool& operator [] (const int i)       { return m_v[i]; }

   void method1();
private:
   vector<bool> m_v;
   queue<int> m_map;
};
void Abool::method1()
{
   for (int i = 0; i < m_components; ++i) m_v.push_back(i);
   for (int i = 0; i < m_components; ++i) { i % 2 == 0 ? m_queue.push(1) : m_queue.push(0); }
}

如您所见,在这种情况下,类型特定的数据结构(queuesmaps 等)仅针对其需要的情况定义(不在 选项 1 的通用类模板中)强>)。但是,由于其特定类型,现在应该在每个派生类中定义 m_v 向量。因此,访问和操作向量的东西应该总是在所有派生类中复制,尽管它们总是相同的(方法v()isValid()operator[] 等)。它也似乎设计得不好。

为此目的最好的设计是什么? 谢谢

【讨论】:

    【解决方案2】:

    使用子操作部分专门化您的类中的特定操作(而不专门化整个类)。

    #include <vector>
    
    namespace detail
    {
      // general concept of indexing into something
      template<class T> struct index_operation;
    
      // indexing into most vectors
      template <class T> struct index_operation<std::vector<T>>
      {
        T& operator()(std::vector<T>& v, std::size_t i) const
        {
          return v[i];
        }
        T const& operator()(std::vector<T> const& v, std::size_t i) const
        {
          return v[i];
        }
      };
    
      // indexing into a vector<bool>
      template <> struct index_operation<std::vector<bool>>
      {
        std::vector<bool>::reference operator()(std::vector<bool>& v, std::size_t i) const
        {
          return v[i];
        }
        std::vector<bool>::const_reference operator()(std::vector<bool> const& v, std::size_t i) const
        {
          return v[i];
        }
      };
    
    
    }
    
    template<typename T>
    class A
    {
        using vector_type = std::vector<T>;
    
    public:
        std::vector<T> v() const { return m_v; }
        decltype(auto) operator [] (const int index) const
        {
          auto op = detail::index_operation<vector_type>();
          return op(m_v, index);
        }
    private:
        std::vector<T> m_v;
    };
    

    【讨论】:

      【解决方案3】:

      这种取决于。

      一般的经验法则是问自己是否“ADouble is-a A”。当它们具有 is-a 关系时,您应该使用继承。

      但是你也有类型依赖,它不是真正的“is-a”关系。

      您还可以同时使用这两种选择:拥有一个具有通用功能的基类,它接受一个模板参数,并拥有一个带有他们需要的附加功能的子类。所以你不需要重新实现所有的类型依赖函数

      所以:

      template<typename T>
      class A
      {
      public:
          vector<T> v() const { return m_v; }
          T operator [] (const int index) const { return m_v[index]; }
          ...
      private:
          vector<T> m_v;
          ...
      };
      
      class ADouble : public A<double>
      {
          ...
      };
      

      顺便说一句:为什么您认为模板会占用更多内存?

      【讨论】:

      • “为什么你认为模板占用更多内存”。因为每个实例都会创建函数的副本。我对 gcc&clang 的经验是,两者都没有将“几乎相同的功能”优化为通用路径和专用路径。如果他们这样做,他们将完全增加运行时间和复杂性。
      • 我想我没有正确解释我的疑问。问题是在这种情况下,共享所有通用功能的基类不存储应该模板化的变量。所以,我可以有一个非模板基类和几个派生类,每个派生类对应一个假设模板类的每个“类型”。
      • @Klaus 啊好的,我没有考虑函数大小,现在我脑子里只有成员变量等。在那种情况下,是的,它需要更多的内存(问题是这个问题在这种情况下有多相关)
      • @Javier 您可能应该更深入地了解您想要设计的内容。这似乎太窄/太少,看不到您需要的信息。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多