【问题标题】:Trying to understand the access operator of a Matrix Multiplication in C++试图理解 C++ 中矩阵乘法的访问运算符
【发布时间】:2021-07-20 19:42:10
【问题描述】:

我正在尝试了解矩阵乘法的访问运算符。

template<typename T> 
class Matrix44 
{ 
public: 
    Matrix44() {} 
    // The next two lines are totally confusing for me
    const T* operator [] (uint8_t i) const { return m[i]; }
    T* operator [] (uint8_t i) { return m[i]; }
    // initialize the coefficients of the matrix with the coefficients of the identity matrix
    T m[4][4] = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}; // Why can you do this m[4][4]
}; 
 
typedef Matrix44<float> Matrix44f; 

所以我的理解是他们定义了一个自己的访问运算符来访问矩阵索引:

Matrx44f mat; 
mat[0][3] = 1.f; 

但这与他们的定义有什么关系

...
const T* operator [] (uint8_t i) const { return m[i]; }
T* operator [] (uint8_t i) { return m[i]; }

非常感谢您帮助 C++ 菜鸟

来源:https://www.scratchapixel.com/lessons/mathematics-physics-for-computer-graphics/geometry/matrices

【问题讨论】:

  • 老实说,这是一个相当糟糕的矩阵实现,它通过其接口泄露了实现细节。 适当的实现不会这样做。
  • 感谢您的重播,但很高兴知道然后我不会深入挖掘:D
  • 您是否缺少指针可以像数组一样被索引的点? (如果p是一个指针,p[k]等价于*(p + k)mat[0]是一个指针。)为了更神秘,考虑你也可以写3[mat[0]] = 1.f;...
  • @KonradRudolph 您是否查看过任何主要实现的 STL 头文件?不确定您对 proper 的定义如何适用。
  • @molbdnilo 哈哈,你的最后一句话很棒。更多的谜团正在增加;)

标签: c++


【解决方案1】:

正如很多人已经指出的那样,这是一个糟糕的矩阵实现:它让你对内部实现做出假设,并且有很多缺陷。但如果我说我不会在研究代码中看到这样的实现,我会撒谎。 ;) 我不想抨击实现,我想简要指出它是如何工作的,因为它显示了 C++ 的一些特殊性。

概述

// Class for a generic data type T (e.g. T = float)
template<typename T> 
class Matrix44 { 
public: 
  // Constructor
  Matrix44() {
    return;
  } 
  // Access operator for constant objects
  const T* operator [] (uint8_t i) const {
    return m[i];
  }
  // Access operator for non-constant objects
  T* operator [] (uint8_t i) {
    return m[i];
  }
  // Declaration of a stack-allocated array as class member
  T m[4][4] 
  // Initialisation of this class member
  = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}};
};

// Alias Matrix44f for a matrix of floats (T = float)
typedef Matrix44<float> Matrix44f; 

堆栈分配的数组

与其他编程语言不同的是,在 C++ 中,可以在 stackheap 上分配数组。堆栈分配的一维数组可以(整数)声明为

int v[3];

同时

int m[2][3];

将是 multidimensional (in this case two-dimensional) row-major array 的声明,可以用它初始化

int m[2][3] = {{1,2},{3,4},{5,6}};

(类似于使用initialiser list 初始化向量向量的方式),然后可以使用类似的语法m[i][j] 访问其元素(也类似于向量向量std::vector&lt;std::vector&lt;T&gt;&gt;,请注意:否越界检查!)。

如您所见,您的类只是一个包装器,用于泛型类型T 的这种堆栈分配的二维数组,

T m[4][4];

所谓的template class。将类实例化为Matrix44&lt;float&gt; 使其成为T = float 的矩阵,而矩阵Matrix44&lt;int&gt; 的基础数据类型将为int。使用底部的typedef Matrix44&lt;float&gt; Matrix44f 创建此数据类型的新别名。所以可以声明一个变量Matrix44f mat

堆栈分配意味着它的大小与堆分配数组相比是非常有限的,尽管对于4x4 数组来说并不是很禁止(但是为什么不将此实现至少扩展到NxN?) .此外,它的编写方式甚至没有适当的构造函数 (Matrix44() {}),而是使用单位矩阵默认初始化

{{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}}

(为什么有人会这样做?)。

运算符 []

C++ 为您提供了重载运算符的可能性。因此,可以定义(重载)基本操作,例如加法operator +、乘法operator *、比较等。类似地,也可以重载[](但只能使用单个参数,因此像Eigen 这样的数字库选择重载 () 运算符)、(),。但是没有[][] 过载之类的东西!为了允许使用[][] 从类外部访问矩阵,类似于堆栈分配的数组,它被选择编写一个运算符[],它返回一个指向第二维第一个元素的指针 在第一维m[i][0]的对应索引i处的底层二维数组:return m[i]等价于return &amp;m[i][0]。然后可以应用指针的运算符[] p[j] 相当于*(p+j)(有关更多详细信息,请参阅here)通过j-entries 移动此指针以访问元素m[i][j]。 (这有点类似于使用vector-of-vectors而不是stack-allocated-array,从运算符[]返回std::vector&lt;T&gt;&amp;,然后使用矢量的[]运算符访问精确元素.)

成员函数可以声明为常量(参见参数列表后面的const),这意味着它们也可以为常量对象调用,而如果未声明为常量,它们只能为非常量对象调用。对于常量对象,调用常量版本,而对于非常量对象,调用非常量实现 (const overloading)。为了允许分配非常量版本

T* operator [] (uint8_t i) {
  return m[i];
}

返回一个可以修改的非常量指针T*,而常量实现返回一个指向常量变量const T*的指针。

const T* operator [] (uint8_t i) const { 
  return m[i];
}

这就是为什么有两个这样的实现的原因。对于常量对象,后一个对象将被调用,它不允许赋值(但您可以使用m[1][2] 读取元素1, 2),而对于非常量对象,第一个允许赋值的对象(例如m[1][2] = 1 将设置元素1, 21)。

【讨论】:

  • 感谢您的详细解释,感谢您抽出宝贵时间给出如此彻底和好的回答!我没想到我的问题会暴露出如此深刻的问题。制定这一切当然需要时间。为此我深表歉意,但我深表感谢!
  • 不客气,如果我能提供帮助总是很高兴!
【解决方案2】:

首先,请记住,此类的唯一价值是为 C 数组提供值语义,而显示的代码尚未实现这一点。 (类似于std::array&lt;T, N&gt; 所做的事情。)

除此之外,这是一个糟糕的实现,它依赖于指针衰减,对 4 元素数组的引用(一旦您获取第一个索引)在您的实现中衰减为指针。 衰减最多会丢失有用的类型信息。

因此,m[i] 返回一个对 C 数组的引用,然后它被类的 operator[] 的返回类型衰减为一个指针。 更好的实现可能只是decltype(auto) operator[](int i){return m[i];}

如果您想更明确地描述正在发生的事情,您可以这样做

    using reference = T(&)[4]; // reference to a 4-element subarray
    reference const operator[](int i) const { return m[i]; }
    reference       operator[](int i)       { return m[i]; }

https://godbolt.org/z/EjbWY691x

此时,最好依赖于现有的良好包装器并简单地说

template<class T> using Matrix44 = std::array<std::array<T, 4>, 4>;

如果对身份的初始化很重要(IMO 不是一个好主意)...

template<typename T> 
class Matrix44 
{ 
public: 
    Matrix44() {} 
    // The next two lines are totally confusing for me
    decltype(auto) operator [] (uint8_t i) const { return m[i]; }
    decltype(auto) operator [] (uint8_t i)       { return m[i]; }
    // initialize the coefficients of the matrix with the coefficients of the identity matrix
    std::array<std::array<T, 4>, 4> m = {{1,0,0,0},{0,1,0,0},{0,0,1,0},{0,0,0,1}};
}; 
 
typedef Matrix44<float> Matrix44f; 

【讨论】:

  • 非常感谢初学者的解释,特别感谢您抽出宝贵的时间写一个答案!特别感谢您将其写到网站上以说明该过程。不知道那个工具。以后肯定对我有帮助! C++ 如此多才多艺真是太酷了,但我发现我要掌握 C++ 还有很长的路要走:D
猜你喜欢
  • 1970-01-01
  • 2014-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-29
  • 2012-11-24
  • 1970-01-01
相关资源
最近更新 更多