【问题标题】:Function that accepts both Eigen Dense and Sparse Matrices接受特征密集矩阵和稀疏矩阵的函数
【发布时间】:2019-12-16 23:32:15
【问题描述】:

我正在努力将稀疏矩阵支持添加到开源数学库中,并且不希望 Dense 和 Sparse 矩阵类型有重复的函数。

以下示例显示了一个add 函数。一个具有两个功能的工作示例,然后两次尝试失败。下面提供了代码示例的神螺栓链接。

我查看了有关编写采用 Eigen 类型的函数的 Eigen 文档,但他们使用 Eigen::EigenBase 的答案不起作用,因为 MatrixBase 和 SparseMatrixBase 都有特定的可用方法,这些方法在 EigenBase 中不存在

https://eigen.tuxfamily.org/dox/TopicFunctionTakingEigenTypes.html

我们使用 C++14,非常感谢您的帮助和您的宝贵时间!!

#include <Eigen/Core>
#include <Eigen/Sparse>
#include <iostream>

// Sparse matrix helper
using triplet_d = Eigen::Triplet<double>;
using sparse_mat_d = Eigen::SparseMatrix<double>;
std::vector<triplet_d> tripletList;

// Returns plain object
template <typename Derived>
using eigen_return_t = typename Derived::PlainObject;

// Below two are the generics that work
template <class Derived>
eigen_return_t<Derived> add(const Eigen::MatrixBase<Derived>& A) {
    return A + A;
}

template <class Derived>
eigen_return_t<Derived> add(const Eigen::SparseMatrixBase<Derived>& A) {
    return A + A;
}

int main()
{
  // Fill up the sparse and dense matrices
  tripletList.reserve(4);
  tripletList.push_back(triplet_d(0, 0, 1));
  tripletList.push_back(triplet_d(0, 1, 2));
  tripletList.push_back(triplet_d(1, 0, 3));
  tripletList.push_back(triplet_d(1, 1, 4));

  sparse_mat_d mat(2, 2);
  mat.setFromTriplets(tripletList.begin(), tripletList.end());

  Eigen::Matrix<double, -1, -1> v(2, 2);
  v << 1, 2, 3, 4;

  // Works fine
  sparse_mat_d output = add(mat * mat);
  std::cout << output;

  // Works fine
  Eigen::Matrix<double, -1, -1> output2 = add(v * v);
  std::cout << output2;

} 

我想要一个同时接受稀疏矩阵和稠密矩阵的函数,而不是两个 add 函数,但是下面的尝试没有成功。

模板模板类型

我的尝试显然很糟糕,但是用模板模板类型替换上面的两个 add 函数会导致模棱两可的基类错误。

template <template <class> class Container, class Derived>
Container<Derived> add(const Container<Derived>& A) {
    return A + A;    
}

错误:

<source>: In function 'int main()':
<source>:35:38: error: no matching function for call to 'add(const Eigen::Product<Eigen::SparseMatrix<double, 0, int>, Eigen::SparseMatrix<double, 0, int>, 2>)'
   35 |   sparse_mat_d output = add(mat * mat);
      |                                      ^
<source>:20:20: note: candidate: 'template<template<class> class Container, class Derived> Container<Derived> add(const Container<Derived>&)'
   20 | Container<Derived> add(const Container<Derived>& A) {
      |                    ^~~
<source>:20:20: note:   template argument deduction/substitution failed:
<source>:35:38: note:   'const Container<Derived>' is an ambiguous base class of 'const Eigen::Product<Eigen::SparseMatrix<double, 0, int>, Eigen::SparseMatrix<double, 0, int>, 2>'
   35 |   sparse_mat_d output = add(mat * mat);
      |                                      ^
<source>:40:52: error: no matching function for call to 'add(const Eigen::Product<Eigen::Matrix<double, -1, -1>, Eigen::Matrix<double, -1, -1>, 0>)'
   40 |   Eigen::Matrix<double, -1, -1> output2 = add(v * v);
      |                                                    ^
<source>:20:20: note: candidate: 'template<template<class> class Container, class Derived> Container<Derived> add(const Container<Derived>&)'
   20 | Container<Derived> add(const Container<Derived>& A) {
      |                    ^~~
<source>:20:20: note:   template argument deduction/substitution failed:
<source>:40:52: note:   'const Container<Derived>' is an ambiguous base class of 'const Eigen::Product<Eigen::Matrix<double, -1, -1>, Eigen::Matrix<double, -1, -1>, 0>'
   40 |   Eigen::Matrix<double, -1, -1> output2 = add(v * v);
      |                                                    ^

我相信这是同一个钻石继承问题:

https://www.fluentcpp.com/2017/05/19/crtp-helper/

使用 std::conditional_t

下面尝试使用conditional_t来推断正确的输入类型

#include <Eigen/Core>
#include <Eigen/Sparse>
#include <iostream>

// Sparse matrix helper
using triplet_d = Eigen::Triplet<double>;
using sparse_mat_d = Eigen::SparseMatrix<double>;
std::vector<triplet_d> tripletList;


// Returns plain object
template <typename Derived>
using eigen_return_t = typename Derived::PlainObject;

// Check it Object inherits from DenseBase
template<typename Derived>
using is_dense_matrix_expression = std::is_base_of<Eigen::DenseBase<std::decay_t<Derived>>, std::decay_t<Derived>>;

// Check it Object inherits from EigenBase
template<typename Derived>
using is_eigen_expression = std::is_base_of<Eigen::EigenBase<std::decay_t<Derived>>, std::decay_t<Derived>>;

// Alias to deduce if input should be Dense or Sparse matrix
template <typename Derived>
using eigen_matrix = typename std::conditional_t<is_dense_matrix_expression<Derived>::value,
 typename Eigen::MatrixBase<Derived>, typename Eigen::SparseMatrixBase<Derived>>;

template <typename Derived>
eigen_return_t<Derived> add(const eigen_matrix<Derived>& A) {
    return A + A;
}

int main()
{
  tripletList.reserve(4);

  tripletList.push_back(triplet_d(0, 0, 1));
  tripletList.push_back(triplet_d(0, 1, 2));
  tripletList.push_back(triplet_d(1, 0, 3));
  tripletList.push_back(triplet_d(1, 1, 4));

  sparse_mat_d mat(2, 2);
  mat.setFromTriplets(tripletList.begin(), tripletList.end());
  sparse_mat_d output = add(mat * mat);

  std::cout << output;
  Eigen::Matrix<double, -1, -1> v(2, 2);
  v << 1, 2, 3, 4;
  Eigen::Matrix<double, -1, -1> output2 = add(v * v);
  std::cout << output2;

} 

这会引发错误

<source>: In function 'int main()':
<source>:94:38: error: no matching function for call to 'add(const Eigen::Product<Eigen::SparseMatrix<double, 0, int>, Eigen::SparseMatrix<double, 0, int>, 2>)'
   94 |   sparse_mat_d output = add(mat * mat);
      |                                      ^
<source>:79:25: note: candidate: 'template<class Derived> eigen_return_t<Derived> add(eigen_matrix<Derived>&)'
   79 | eigen_return_t<Derived> add(const eigen_matrix<Derived>& A) {
      |                         ^~~
<source>:79:25: note:   template argument deduction/substitution failed:
<source>:94:38: note:   couldn't deduce template parameter 'Derived'
   94 |   sparse_mat_d output = add(mat * mat);
      |                                      ^
<source>:99:52: error: no matching function for call to 'add(const Eigen::Product<Eigen::Matrix<double, -1, -1>, Eigen::Matrix<double, -1, -1>, 0>)'
   99 |   Eigen::Matrix<double, -1, -1> output2 = add(v * v);
      |                                                    ^
<source>:79:25: note: candidate: 'template<class Derived> eigen_return_t<Derived> add(eigen_matrix<Derived>&)'
   79 | eigen_return_t<Derived> add(const eigen_matrix<Derived>& A) {
      |                         ^~~
<source>:79:25: note:   template argument deduction/substitution failed:
<source>:99:52: note:   couldn't deduce template parameter 'Derived'
   99 |   Eigen::Matrix<double, -1, -1> output2 = add(v * v);

这似乎是因为无法像此链接那样推断出依赖类型的依赖参数。

https://deque.blog/2017/10/12/why-template-parameters-of-dependent-type-names-cannot-be-deduced-and-what-to-do-about-it/

Godbolt 示例

下面的神螺栓可以玩上面的所有实例

https://godbolt.org/z/yKEAsn

有没有办法只拥有一个功能而不是两个?我们有很多函数可以同时支持稀疏矩阵和稠密矩阵,因此避免代码重复会很好。

编辑:可能的答案

@Max Langhof 建议使用

template <class Mat>
auto add(const Mat& A) {
 return A + A; 
}

auto 关键字对 Eigen 来说有点危险

https://eigen.tuxfamily.org/dox/TopicPitfalls.html

但是

template <class Mat> 
typename Mat::PlainObject add(const Mat& A) { 
    return A + A; 
}

有效,但我不完全确定为什么在这种情况下返回普通对象有效

编辑编辑

有几个人提到了auto 关键字的使用。遗憾的是,Eigen 不能很好地与 auto 配合使用,正如 C++11 的第二个中所引用的和下面链接中的 auto 一样

https://eigen.tuxfamily.org/dox/TopicPitfalls.html

在某些情况下可以使用 auto ,但我想看看是否有一个通用的 auto'ish 方式来抱怨 Eigen 的模板返回类型

对于带有自动的段错误示例,您可以尝试将添加替换为

template <typename T1>
auto add(const T1& A) 
{
    return ((A+A).eval()).transpose();
}

【问题讨论】:

  • 你使用的是什么 C++ 版本?
  • C++14,谢谢,我会在帖子里补充的!
  • 你不能只做template &lt;class Mat&gt; auto add(const Mat&amp; A) { return A + A; } 的任何特殊原因(可能有一些 SFINAE 将其限制为特征矩阵)?或者这是否会以某种方式遇到表达式模板的问题?
  • ...大声笑。让我试试这个!我唯一担心的是 auto 关键字,因为 Eigen 说不要在他们的文档中使用它。 eigen.tuxfamily.org/dox/TopicPitfalls.html
  • 啊!因此,如果我返回的 Mat::PlainObject 确实有效!虽然我需要检查是否有任何问题,因为这对我来说并不完全有意义。让我探索一下这是否会导致任何奇怪的错误,如果不是,我会接受这个作为答案!

标签: c++ templates eigen multiple-inheritance


【解决方案1】:

如果你想传递EigenBase&lt;Derived&gt;,你可以使用.derived()提取底层类型(本质上,这只是转换为Derived const&amp;):

template <class Derived>
eigen_return_t<Derived> add(const Eigen::EigenBase<Derived>& A_) {
    Derived const& A = A_.derived();
    return A + A;
}

更高级,对于这个特定的示例,由于您使用了两次A,您可以使用内部评估器结构来表达:

template <class Derived>
eigen_return_t<Derived> add2(const Eigen::EigenBase<Derived>& A_) {
    // A is used twice:
    typedef typename Eigen::internal::nested_eval<Derived,2>::type NestedA;
    NestedA A (A_.derived());
    return A + A;
}

这样做的好处是,当以A_ 传递一个产品时,它在评估A+A 时不会被评估两次,但如果A_ 类似于Block&lt;...&gt;,它就不会被不必要地复制。但是,不建议使用 internal 功能(其 API 可能随时更改)。

【讨论】:

  • 有趣!我要上飞机了,等我一分钟就试试这个
  • 这似乎有效!一个 Q,因为看起来你对 Eigen 了解很多。 eigen_return_t&lt;Derived&gt; 是设置返回类型的好方法还是应该是其他东西?例如,在我上面编辑的案例中,return((A+A).eval()).transpose();?
  • 另外,您可以在这里看到您的劳动成果,这是我正在处理的稀疏矩阵设计文档的链接github.com/SteveBronder/design-docs/blob/spec/sparse-matrices/…
  • 返回 Derived::PlainObject 始终是一个安全的选择,即它将按值返回。在许多情况下,这可能会导致不必要的临时性,但返回自定义表达式树可能会过大(如果你想走这条路,你应该研究 Eigen 的内部结构,不幸的是它没有很好的文档记录)。但是按值返回通常至少会给你返回值优化(特别是如果你只有一个返回的路径),即没有冗余副本。
  • ?? 太好了,我已将此答案添加到设计文档中。希望有一种方法可以删除有关派生内容的样板文件
【解决方案2】:

你的编译器的问题如下:

无法推断出模板参数“Derived”

为Derived 传递所需的类型应该可以工作,如下所示:

add<double>(v * v)

但我不确定,因为Eigen::Matrix 与我看来的Eigen::MatrixBase 类型不同。

但是,如果您对编译器的类型限制较少,它将能够找出类型:

template <typename T>
auto add(const T& A) {
    return A + A;
}

编辑:

刚刚在 cmets 中看到该解决方案已经发布,并且 Eigen 文档建议不要使用 auto。我对 Eigen 不熟悉,但是从浏览文档中可以看出,Eigen 可能会产生表示表达式的结果 - 例如将矩阵加法表示为算法的对象;不是矩阵加法结果本身。在这种情况下,如果你知道 A + A 的结果是 T 类型(我认为它实际上应该是 operator+),你可以这样写:

template <typename T>
T add(const T& A) {
    return A + A;
}

在矩阵示例中,这应该强制返回一个矩阵结果;不是表示表达式的对象。但是,由于您最初使用的是eigen_result_t,因此我不能 100% 确定。

【讨论】:

  • 感谢您的回答!如果可能的话,我想避免为编译器提供被调用者的模板类型。如果将auto 替换为T:PlainObject,则上述解决方案有效!一旦我调查T::PlainObject 是否正常,那么这是一个很好的答案!希望有一些方法可以给你和@max 信用,因为他在 cmets 中的回答相同。
  • 是的,当 Eigen 开始谈论 auto 及其返回类型时,我的大脑变成了 dogWithChemistrySetMeme.jpg PlainObject 返回有点道理,但我想弄清楚他们为什么不在代码中使用它
  • 确实如此,所以我关于临时工的推理毫无意义。我会将其更改为 Eigen 文档中的“抽象表达式”案例。
  • T::PlainObject 对我来说似乎有点不直观。不是简单的T 也适用于返回类型吗?
  • ^正是我想避免它的原因。它有效,但某些事情似乎显然不直观/愚蠢
【解决方案3】:

我还没有理解您的所有代码和 cmets。无论如何,您的问题似乎归结为找到一种方法来编写一个可以接受多种矩阵类型的函数。

template <typename T>
auto add(const T& A)
{
    return 2*A;
}

您还可以添加 2 个不同类型的矩阵:

template <typename T1, typename T2>
auto add(const T1& A, const T2& B) -> decltype(A+B) // decltype can be omitted since c++14
{
    return A + B;
}

然后,add(A,A) 给出与add(A) 相同的结果。但我认为带有 2 个参数的 add 函数更有意义。而且它更加通用,因为您可以将稀疏矩阵与密集矩阵相加。

int main()
{
    constexpr size_t size = 10;
    Eigen::SparseMatrix<double> spm_heap(size,size);
    Eigen::MatrixXd m_heap(size,size);
    Eigen::Matrix<double,size,size> m_stack; 

    // fill the matrices

    std::cout << add(spm_heap,m_heap);
    std::cout << add(spm_heap,m_stack);

    return 0;
}

编辑

关于您声明 auto 不应与 Eigen 一起使用的编辑。这很有趣!

template <typename T>
auto add(const T& A) 
{
    return ((A+A).eval()).transpose();
}

这会产生一个segfault。为什么? auto 确实很好地推断了类型,但推断的类型不是decltype(A),而是该类型的reference。为什么?我一开始以为是因为返回值周围的括号(如果有兴趣,请阅读here),但似乎是因为transpose 函数的返回类型。

无论如何,克服这个问题很容易。按照您的建议,您可以删除 auto:

template <typename T>
T add(const T& A) 
{
    return ((A+A).eval()).transpose();
}

或者,您可以使用auto,但指定所需的返回类型:

template <typename T>
auto add(const T& A) -> typename std::remove_reference<decltype(A)>::type // or simply decltype(A.eval())
{
    return ((A+A).eval()).transpose();
}

现在,对于这个特定的add 函数,第一个选项(省略auto)是最好的解决方案。但是,对于另一个接受 2 个不同类型参数的 add 函数,这是一个很好的解决方案:

template <typename T1, typename T2>
auto add(const T1& A, const T2& B) -> decltype((A+B).eval())
{
    return ((A+B).eval()).transpose();
}

【讨论】:

  • 感谢您的回答!我编辑了上面的帖子以链接到他们说不要使用 auto 的 Eigen 文档。您的答案适用于此处的简单添加功能,我想我需要更新 Q 以显示自动失败的示例
  • 我添加了一个帖子来展示 auto 何时导致 seg 错误的示例
猜你喜欢
  • 2013-09-25
  • 1970-01-01
  • 2016-06-25
  • 2014-03-06
  • 2021-02-23
  • 2016-10-09
  • 2013-08-27
  • 1970-01-01
  • 2023-03-29
相关资源
最近更新 更多