继承vs类模板专门化.设计的家伙

Inheritance vs class template specialization. Design dude

本文关键字:专门化 vs 继承      更新时间:2023-10-16

我在实现设计上遇到了麻烦。我希望你能帮助我。假设我有以下类

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<bool> m_foo;,并更改几个应该使用这些新属性的方法。

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

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

选项2:模板特化。我可以为每种类型专门化类模板,因此只提供根据T类型而变化的特定方法的实现。然而,这强制在模板类中存储大量只在T是特定类型时使用的东西,即m_foo向量只在T类型是double时使用(在建议的示例中)。因此,我是在浪费记忆。此外,实现一个模板类并为几乎大多数模板类型提供模板类专门化,并存储仅用于特定类型的特定属性,似乎不是很优雅,甚至不一致。

我不知道我是否把我的问题解释清楚了。希望如此。

提前谢谢你。哈维尔。

这得看情况。

一般的经验法则是问自己"double是不是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>
{
    ...
};

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

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

#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;
};

我将尝试用一个与我的实际问题密切相关的例子来阐述这个问题,尽管这将成为一个较长的帖子。

Option1:考虑以下基于模板和模板专门化的类实现。

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类型有特定的实现,并且也需要根据该类型使用特定的数据结构(queues, maps, vectors)。我看到非常非常丑陋的队列,映射,向量等在类的定义,虽然如果他们只在具体情况下使用取决于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向量的特定类型,现在应该在每个派生类中定义它。因此,访问和操作vector的东西应该总是在所有派生类中复制,尽管它们总是相同的(方法v(), isValid(), operator[]等)。它似乎也没有设计好。

这个目的的最佳设计是什么?谢谢你