【问题标题】:Different template class realizations but same member functions不同的模板类实现但相同的成员函数
【发布时间】:2020-12-09 12:36:47
【问题描述】:

我有一个模板矩阵类,其中包含一些典型的成员函数,例如inversedeterminantoperator* 等。我想在两个模板实现中重用这些成员函数的代码 固定-和动态尺寸矩阵。 这可能吗?如果是,如何?

在下面的代码中,与 Eigen 一样,我使用“-1”作为动态尺寸。是的,我知道我可以为此使用库,但对于我的应用程序,这是不可行的。 由于应用程序 (CUDA) 的性质,无法使用标准功能

是否可以根据模板参数制作不同成员变量大小的模板类? 比如Dynamic_Rows = -1Dynamic_Cols = -1,那么数据就是T **data,但是否则为T data[Rows][Cols]

到目前为止,我有一个用于动态大小矩阵的模板类(下面的“minimal”示例,请注意代码可能会出现错误/错误,因为我对“高级”类比较陌生模板)。

但是在固定大小的矩阵实例化的情况下,我希望有一个固定大小的数据成员变量。

template<class T, int Dynamic_Rows, int Dynamic_Cols>
class CMatrix<T, -1, -1>
{
private:
   size_t n_rows, n_cols;
   T** data;

   void allocate_data();
   void deallocate_data();

public:
   CMatrix(const size_t n_rows, const size_t n_cols);
   CMatrix(const CMatrix& other);
   ~CMatrix();
   CMatrix& operator=(const CMatrix& rhs);
   CMatrix exp() const;
};

例如下面exp()函数的代码

template <class T, int Dynamic_Rows, int Dynamic_Cols>
CMatrix<T, -1, -1> CMatrix<T, -1, -1>::exp() const
{
   CMatrix<T, -1, -1> result(n_rows, n_cols);
   for (size_t i = 0; i < n_rows; i++)
   {
      for (size_t j = 0; j < n_cols; j++)
      {
         result.data[i][j] = exp(result.data[i][j]);
      }
   }
   return result;
}

我现在唯一能想到的同时允许动态和固定大小的矩阵是基本上实现该类的另一个模板

template<class T, size_t Rows, size_t Cols>
class CMatrix<T, Rows, Cols>
{
private:
   size_t n_rows = Rows, n_cols = Cols;
   T data[Rows][Cols];

public:
   CMatrix() {}
   CMatrix(const CMatrix& other);
   CMatrix& operator=(const CMatrix& rhs);
   CMatrix exp() const;
};

使用可变参数模板

template<class T, int...> class CMatrix;

但是那样只会复制我的大多数成员函数的代码!

【问题讨论】:

  • 我会将 Matrix 类拆分为数据部分(您以动态或静态形式处理数据)和“逻辑”部分(您在 data[x][y] 上进行操作,无论它是动态的还是静态的)。
  • 如果您得到重复的成员函数,请将它们提取到一个公共基类中。这对于 CRTP 来说是一个很好的案例(操作 data 的代码是 dame,但 data 的类型不同)。但是您应该知道T** data 对性能不利。如果性能很重要,您想使用一维数组,并自己进行二维地址转换。
  • @n.'pronouns' m 我对 CRTP 不是很熟悉,所以在这里看不到如何使用它的直接解决方案。想用简短的回答来澄清一下吗?基类是否可以访问函数中使用的数据(固定的或动态的)?是的,我将从双指针变为单指针以获得更好的性能,这只是一个示例。

标签: c++ c++11 templates template-specialization class-template


【解决方案1】:

这是使用 CRTP 进行此类专业化的一种非常通用的方法。它不要求有一个简单的单一条件可用于选择数据成员的实现,而其余部分保持不变。

template <class T, class Impl> class MatrixBase {

   // data is NOT defined here
   Impl exp() const
   {
        Impl& self = static_cast<Impl&>(*this);             // <-- this is magic
        Impl result = self;                                 // <-- this is magic
        for (size_t i = 0; i < n_rows; i++)
        {
            for (size_t j = 0; j < n_cols; j++)
            {
                result.data[i][j] = exp(result.data[i][j]); // <-- data is defined in Impl
            }
        }
        return result;
    }
    // other functions that do not depend on actual type of data
};

template <class T>
class DynamicMatrix : public MatrixBase<T, DynamicMatrix<T>> {
    T** data;
    // define constructors etc here
};

template <class T, int Rows, int Cols>
class StaticMatrix : public MatrixBase<T, StaticMatrix<T, Rows, Cols>> {
    T[Rows][Cols] data;
    // define constructors etc here
};

现在您拥有StaticMatrixDynamicMatrix,如果您愿意,可以将它们统一为一个别名模板。

template <class T, int Rows, int Cols>
using CMatrix = std::conditional_t <(Rows >= 0 && Cols >= 0),
                                     StaticMatrix<T, Rows, Cols>,
                                     DynamicMatrix<T>
                                   >;

【讨论】:

  • 谢谢!这对我来说是一个合适的解决方案。但是,你能澄清一下“神奇”的代码行吗?这基本上是在运行时将派生类数据提取到合适的类型 Impl 中吗?然后我需要在每个函数中执行那些神奇的代码行来获取数据吗?并且 static_cast 不是 std 库的一部分,因此我需要自定义实现吗?
  • “魔法”将this类型 提升为具有data 成员的派生类的类型。它没有运行时成本。没关系,因为MatrixBase 被设计用作CRTP 基类,所以它知道它是从Impl 派生的。
  • @A_Man static_cast 可以替换为 c 风格的演员表。 然后我需要在每个函数中执行那些神奇的代码行来获取数据 - 要么这样,要么在内联 getter 中编写第一行。
  • @n。 '代词' m 后续问题:假设 MatrixBase 中的成员函数的功能取决于 Impl 的类型。那么解决方案是在派生类中单独定义函数吗?例如,如果需要创建一个临时 Impl 结果(n_rows,n_cols),这对于 StaticMatrix 的情况是不可能的,因为 Impl 在编译时被定义为给定 ,可能与 n_rows 和 n_cols 不对应。
  • @A_Man 您只需在其中创建一个 DynamicMatrix 而不是 Impl。只有在需要 Impl 与当前对象的类型相同时才创建它。
【解决方案2】:

有没有可能....[...] ...当Dynamic_Rows = -1Dynamic_Cols = -1,那么数据只是T **data,否则它是T data[Rows][Cols]

您可以提供一个特征类型,其特化为您提供CMatrix 的正确类型

template<typename T, int Rows, int Cols> struct type_helper final {
    static_assert(Rows >= 0 && Cols >= 0, "Rows and COls must be valid!");
    using type = T[Rows][Cols];
};

template<typename T> struct type_helper<T, -1, -1>  final {
    using type = T**;
};

现在上课了

template<class T, int Dynamic_Rows = -1, int Dynamic_Cols = -1>
class CMatrix /* final */
{
    // get the member Type like this!
    using Type = typename type_helper<T, Dynamic_Rows, Dynamic_Cols>::type;

    Type data;
public:

};

(See a demo online)

【讨论】:

  • 整洁!我会考虑使用这个版本,因为它不需要太多改动。
  • @A_Man 是的.. 这是一个加号。另请注意,如果您只保留一个CMatrix,则始终需要考虑 mem 函数中两种类型的代码更改。在 C++17 中,您可以在编译时使用 if constexpr 检查 Type,并且可以丢弃 false 语句下的代码,同样您将拥有一个成员函数管理两种不同的类型。 C++17 之前的编译器,您可能需要使用 SFINAE 或 CMatrix 的专业化。
  • 我更喜欢专门化不同的部分,因为有几个地方(构造函数/析构函数,可能是基本访问器)需要分支,特别是构造函数和初始化器列表成员初始化 if constexpr 不能直接使用(仍然有 lambda 作为解决方法)。
【解决方案3】:

您可以使用std::conditional_t 根据一些编译时常量选择一种或另一种类型,而无需复制类。

但是 - 我曾经做过非常相似的事情,发现使用固定大小的数据结构(std::array 而不是 std::vector)在实际用例中几乎没有改进。 固定大小的数组(std::arraytype[fixed_size])的一个非常大的优势是编译器能够优化很多操作,但如果数组很大,好处可能会变得微不足道。

另外,你应该考虑n。 '代词' m. 的评论 - 特别是如果您使用的是 GPU(它们往往需要在内存中连续的数据结构)。不要使用T** data,而是使用单个std::vector&lt;T&gt;,并在每个维度中使用一个额外的std::array&lt;size_t, number_of_dimensions&gt;。然后你可以用这样的方式翻译索引:

using Indices = std::array<size_t, number_of_dimensions>;

template <unsigned int n = number_of_dimensions>
static inline size_t indices_to_offset(Indices const & indices,
                                       Indices const & dimensions){
    if constexpr (n == 0) return 0;
    else {
        size_t const next_index_number = indices[n-1];
        assert(next_index_number < dimensions[n-1]);
        return next_index_number +
            dimensions[n-1]*indices_to_offset<n-1>(indices, dimensions);
    }
}

template <unsigned int n = number_of_dimensions>
static inline Indices offset_to_indices(size_t const offset,
                                        Indices const & dimensions,
                                        Indices indices = Indices()){
    if constexpr (n == 0) return indices;
    else {
        size_t const fast_dimension_size = dimensions[n-1];
        indices[n-1] = offset % fast_dimension_size;
        return offset_to_indices<n-1>(offset/fast_dimension_size , dimensions, indices);
    }
}

【讨论】:

  • 感谢您的回复。为了提高性能,我肯定会从双指针变为单指针。忘了提,但我一般不能使用 std:: 或 std 库功能,因为这段代码应该在 GPU 上运行,因此我什至费心这样做。
  • 如果没有标准库,最好的办法是自己实现 contidional_t(请参阅 JeJo 的答案)。但是,正如我所提到的,我的建议是根本不要打扰它。一旦你有了一个动态数组的实现,你可以尝试切换到固定大小,看看你是否在运行时间上有任何改进。见this
  • @JeJo 我想到的具体用途是std::conditional_t&lt;Dynamic_Rows &gt; 0, std::array&lt;T, Dynamic_Rows&gt;, std::vector&lt;T&gt; &gt;,可能嵌套在另一个std::conditional_t中。
【解决方案4】:

将我的评论变成答案(使用Vector 而不是Matrix):

拆分你的班级,这样你就有了专攻特定班级的部分,以及共同的部分,例如:

// helper to allow variadic constructor without pitfall.
constexpr struct in_place_t{} in_place{};

template <typename T, std::size_t DIM>
class VectorData
{
protected: // You might even make it private and give the common accessor in public part
           // To avoid to use interface not shared with std::vector
    std::array<T, DIM> data;
public:
    VectorData() = default;

    template <typename ... Ts>
    VectorData(in_place_t, Ts&&... args) : data{std::forward<Ts>(args)...} {}
};

template <typename T>
class VectorData<T, std::size_t(-1)>
{
protected: // You might even make it private and give the common accessor in public part
           // To avoid to use interface not shared with std::array
    std::vector<T> data;
public:
    explicit VectorData(std::size_t size) : data(size) {}

    template <typename ... Ts>
    VectorData(in_place_t, Ts&&... args) : data{std::forward<Ts>(args)...} {}
};

然后是公共部分:

template <typename T, std::size_t DIM = std::size_t(-1)>
class Vector : public VectorData<T, DIM>
{
public:
    using VectorData<T, DIM>::VectorData;

    Vector(std::initializer_list<T> ini) : VectorData<T, DIM>(in_place, ini) {}

#if 1 // Those might go in `VectorData` to avoid to use `data` directly
    std::size_t size() const { return this->data.size(); }
    T& operator[](std::size_t i) { return this->data[i]; }
    const T& operator[](std::size_t i) const { return this->data[i]; }
#endif

    // Example of methods which don't care underlying container
    template <std::size_t DIM2>
    Vector& operator += (const Vector<T, DIM2>& rhs) {
        assert(this->size() == rhs.size());
        
        for (std::size_t i = 0; i != this->size(); ++i) {
            (*this)[i] += rhs[i];
        }
        return *this;
    }

    template <std::size_t DIM2>
    friend Vector operator + (Vector lhs, const Vector<T, DIM2>& rhs) {
        return lhs += rhs;
    }
};

Demo

【讨论】:

    猜你喜欢
    • 2020-04-13
    • 1970-01-01
    • 1970-01-01
    • 2014-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多