C++矩阵类层次结构

9
一个矩阵软件库应该有一个根类(例如MatrixBase),从中更多的特定(或更受限制)的矩阵类(例如SparseMatrixUpperTriangluarMatrix等)派生出来吗?如果是这样,那么派生类应该公开/保护/私有地派生吗?如果不是,它们应该与封装通用功能的实现类组合在一起,否则无关?还是其他什么?
我和一位软件开发同事进行了这方面的讨论(我本身不是),他提到从更普遍的类(例如,他使用了将Circle类从Ellipse类派生出来不是一个好主意的例子,类似于矩阵设计问题)派生出更受限制的类是常见的编程设计错误,即使一个SparseMatrix“是一个”MatrixBase。基类和派生类呈现的接口应该对基本操作相同;对于专业操作,派生类将具有额外的功能,这可能无法为任意MatrixBase对象实现。例如,我们只能为PositiveDefiniteMatrix类对象计算Cholesky分解;但是,乘以标量应该对基类和派生类起作用。此外,即使底层数据存储实现不同,operator()(int,int)也应该对任何类型的矩阵类按预期工作。
我已经开始查看一些开源矩阵库,看起来这有点混乱(或者可能是我正在查看混杂的库)。我计划协助重构一个数学库,其中这是一个争议点,我想要意见(除非这个问题真的有客观的“正确”答案),关于哪种设计哲学最好,以及任何合理方法的利弊。
6个回答

4

当你可以根据椭圆接口修改一个维度时,椭圆的子类Circle(或矩形的子类Square)存在问题,因此圆不再是圆形(正方形也不再是正方形)。

如果只允许不可修改的矩阵,则安全,并且您可以以自然方式构建类型层次结构。


是的,我更喜欢不可变数据,尽管我知道这在 C++ 中并不常见。 - starblue
在实际情况下讨论Cicle/Ellipse示例中的漏洞问题,而不涉及两者之间关系的概念理论,这是值得赞赏的。基于实际需求(如维护不变量),很容易理解这些问题。 - stinky472

1

像这样基于继承的设计中需要注意的主要问题是SLICING。

假设MatrixBase定义了一个非虚拟赋值运算符。它复制了所有矩阵子类共有的数据成员。您的SparseMatrix类定义了其他数据成员。现在当我们编写以下代码时会发生什么?

SparseMatrix sm(...);
MatrixBase& bm = sm;
bm = some_dense_matrix;

这段代码没有多少意义(试图通过在基类中定义的运算符直接将DenseMatrix分配给SparseMatrix),并且容易出现各种令人讨厌的切片行为,但如果您通过MatrixBase*/MatrixBase&提供可访问的赋值运算符,这是代码的一个脆弱方面,并且很有可能在某个地方发生。即使我们有:

SparseMatrix sm(...);
MatrixBase& bm = sm;
bm = some_other_sparase_matrix;

...由于赋值运算符不是虚拟的,我们仍然存在切片问题。如果没有一个共同的基类的继承,我们可以提供赋值运算符来有意义地将密集矩阵复制到稀疏矩阵中,但是通过一个共同的基类来尝试这样做容易出现各种问题。

通常应避免为基类使用赋值运算符!想象一种情况,狗和猫继承自哺乳动物,而哺乳动物提供了一个赋值运算符,无论是否虚拟。这将意味着我们可以将狗分配给猫,这毫无意义,即使运算符是虚拟的,也很难为将哺乳动物分配给其他哺乳动物提供任何有意义的行为。

假设我们尝试通过在狗中实现赋值运算符,使其只能被分配给其他狗来改善情况。现在当我们从狗继承以创建吉娃娃和杜宾犬时会发生什么?我们不应该能够将吉娃娃分配给杜宾犬,因此原始情况递归地重复,直到您确信已到达继承层次结构的叶节点(遗憾的是C++没有final关键字来防止任何进一步的继承)。

常见的“圆形继承椭圆”例子也存在同样的问题。 圆形可能需要宽度和高度匹配:这是该类希望保持的不变性,但任何人都可以简单地获取指向Circle对象的基指针(Ellipse*)并违反该规则。

如果有疑问,请避免使用继承,因为这是C++和支持面向对象编程的任何语言中最常被误用的功能之一。您可以尝试通过提供运行时机制来确定分配给另一个子类的子类的类型,并仅允许匹配类型来解决该问题,但现在您正在执行大量额外的工作并产生运行时开销。更好的方法是对于继承层次结构,完全避免使用赋值运算符,并依靠像克隆这样的方法来生成副本(原型模式)。

因此,如果您选择将继承层次结构纳入矩阵类中,您应认真考虑继承的(最有可能的短期)优势是否超过长期劣势。您还应确保避免所有可能发生切片的情况,这对于矩阵库可能非常困难,而不影响其可用性和效率。


1

呵呵呵。一开始我读到你的朋友说圆应该是椭圆,于是写了一篇长篇大论来反驳他们。

你应该听取你朋友的建议,但我希望他们没有说SparseMatrix“是一个”MatrixBase。这个术语在现实世界和建模世界中有不同的含义。“是一个”在建模世界中意味着遵循Liskov替换原则(查一下!)。或者它意味着SparseMatrix必须遵循MatrixBase的契约,即成员函数不需要任何额外的前提条件,并且必须满足不少于后置条件。

我不知道这如何适用于矩阵问题,但如果你查看我在上一段中使用的术语(LSP和Design by Contract),那么你应该已经走上了解决问题的道路。

在你的情况下,可能适用的一种方法是将层次结构中的各种共性抽象为接口。然后在那些正确响应它们的类中继承这些接口。这将允许你编写应该允许常见用法的函数,同时仍然保留变化过大的分离。


感谢您提供有关参考资料的指针。我没有意识到这个问题有一个完整的维基百科文章:http://en.wikipedia.org/wiki/Circle-ellipse_problem - bpw1621

1

这是一个不错的问题,但我还不确定您想要评估的指标是什么。

值得一提的是,我目前使用最多的一个矩阵库是Armadillo,它确实有一个常见的Base对象,使用了“奇异递归模板模式”。我相信Eigen(另一个最近和大量使用模板的矩阵库)也是如此。


那么CRTP在这里究竟有什么帮助呢?它不是有效地下转换模式(通常在SO上称为代码异味)吗?例如,template< typename UpperTriangularMatrix > class MatrixBase { void interface() { static_cast< UpperTriangularMatrix >(this)->implementation(); } }; UpperTriangularMatrix : MatrixBase< UpperTriangularMatrix > { void implementation(); };因此,如果接口和实现引用矩阵乘法,那么想法是接口调用将委托给派生类中的正确实现调用吗?使用时,客户端只调用接口? - bpw1621
有没有办法像帖子一样格式化注释?上面的反引号无法正常工作。 - bpw1621
你需要跟Conrad讨论Armadillo的设计,我只是使用它。至于格式,我也很遗憾,并不知道如何解决。 - Dirk Eddelbuettel
谢谢您的跟进。出于好奇,关于Matrix类的设计,您决定了什么? - Dirk Eddelbuettel
我倾向于采用将它们实现为单独的类(例如,多态无关)并共享实现类的设计。但我仍然需要说服一个CCB,这是重构的最佳方式,我们拭目以待。 - bpw1621
显示剩余2条评论

0

是否有一个矩阵基类,其中包含方法,允许您构建特定的矩阵,这种可能性会很有用吗?例如,像这样的东西(一个非常简单的例子):

MatrixClass m;
m.buildRotationMatrix(/*params*/)
// Now m is a rotation matrix

这在OpenSceneGraph框架中使用,并且对我们的目的非常有效。然而,构建方法仅仅是旋转或反转之类的操作。但我认为它可以避免派生许多矩阵子类的问题。


0
如果有足够的常见方法和成员需要一个基类,那么就应该使用继承。我不会将基类用作所有矩阵的公共类型,而是用作常见方法和成员的容器(使构造函数受保护)。
与Java不同,不是每个类或结构都需要一个基类。记住简单性;复杂性会使项目变得更长、更难管理和更难以正确实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接