教导Google-Test如何打印Eigen矩阵

21

介绍

我正在使用谷歌的测试框架Google-Mock编写对Eigen矩阵的测试,如另一个问题中所讨论的。

使用以下代码,我能够添加一个自定义的Matcher,以匹配给定精度的Eigen矩阵。

MATCHER_P2(EigenApproxEqual, expect, prec,
           std::string(negation ? "isn't" : "is") + " approx equal to" +
               ::testing::PrintToString(expect) + "\nwith precision " +
               ::testing::PrintToString(prec)) {
    return arg.isApprox(expect, prec);
}

这段代码将通过它们的 isApprox 方法比较两个 Eigen 矩阵,如果它们不匹配,Google-Mock 将打印相应的错误消息,其中包含矩阵的期望值和实际值。或者,至少应该是这样...

问题

看下面这个简单的测试用例:

TEST(EigenPrint, Simple) {
    Eigen::Matrix2d A, B;
    A << 0., 1., 2., 3.;
    B << 0., 2., 1., 3.;

    EXPECT_THAT(A, EigenApproxEqual(B, 1e-7));
}

这个测试会失败,因为AB不相等。不幸的是,相应的错误信息看起来像这样:

gtest_eigen_print.cpp:31: Failure
Value of: A
Expected: is approx equal to32-byte object <00-00 00-00 00-00 00-00 00-00 00-00 00-00 F0-3F 00-00 00-00 00-00 00-40 00-00 00-00 00-00 08-40>
with precision 1e-07
  Actual: 32-byte object <00-00 00-00 00-00 00-00 00-00 00-00 00-00 00-40 00-00 00-00 00-00 F0-3F 00-00 00-00 00-00 08-40>

如您所见,Google-Test打印的是矩阵的十六进制转储,而不是更好的值表示方式。
Google文档对于自定义类型的值打印说:

该打印机知道如何打印内置C++类型、本地数组、STL容器和支持<<运算符的任何类型。对于其他类型,它会打印值中的原始字节,并希望您用户能够弄清楚。

Eigen矩阵附带一个operator<<。然而,Google-Test或者说C++编译器忽略了它。据我理解,原因如下:这个运算符的签名为(IO.h (line 240))
template<typename Derived>
std::ostream &operator<< (std::ostream &s, const DenseBase<Derived> &m);

换句话说,它需要一个const DenseBase <Derived>&。 Google-test十六进制转储默认打印机是模板函数的默认实现。 您可以在此处找到实现此处。(从PrintTo开始跟踪调用树,以查看是否为此情况,或者证明我是错误的。;))

因此,Google-Test默认打印机更匹配,因为它采用const Derived&,而不仅仅是其基类const DenseBase<Derived>&


我的问题

我的问题是:如何告诉编译器优先选择Eigen特定的operator<<而不是Google-test十六进制转储? 假设我无法修改Eigen矩阵的类定义。


我的尝试

到目前为止,我尝试了以下几件事。

定义一个函数

template <class Derived>
void PrintTo(const Eigen::DensBase<Derived> &m, std::ostream *o);

使用同样的原因,operator<<也无法正常工作。

我发现唯一有效的方法是使用Eigen的插件机制

需要使用一个名为eigen_matrix_addons.hpp的文件:

friend void PrintTo(const Derived &m, ::std::ostream *o) {
    *o << "\n" << m;
}

并且以下是包含指令

#define EIGEN_MATRIXBASE_PLUGIN "eigen_matrix_addons.hpp"
#include <Eigen/Dense>

该测试将生成以下输出:
gtest_eigen_print.cpp:31: Failure
Value of: A
Expected: is approx equal to
0 2
1 3
with precision 1e-07
  Actual:
0 1
2 3

有什么问题吗?

对于Eigen矩阵而言,这可能是一个可接受的解决方案。然而,我知道很快就需要将同样的东西应用到其他模板类上,这些类不像Eigen那样提供插件机制,而且我没有直接访问它们的定义。

因此,我的问题是:有没有一种方法可以指示编译器使用正确的operator<<PrintTo函数,而不必修改类的定义本身?


完整代码

#include <Eigen/Dense>

#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <gmock/gmock-matchers.h>

// A GMock matcher for Eigen matrices.
MATCHER_P2(EigenApproxEqual, expect, prec,
           std::string(negation ? "isn't" : "is") + " approx equal to" +
               ::testing::PrintToString(expect) + "\nwith precision " +
               ::testing::PrintToString(prec)) {
    return arg.isApprox(expect, prec);
}

TEST(EigenPrint, Simple) {
    Eigen::Matrix2d A, B;
    A << 0., 1., 2., 3.;
    B << 0., 2., 1., 3.;

    EXPECT_THAT(A, EigenApproxEqual(B, 1e-7));
}

编辑:进一步的尝试

我采用了SFINAE方法取得了一些进展。

首先,我为Eigen类型定义了一个特征(trait)。有了它,我们可以使用std::enable_if只提供模板函数给满足此特征的类型。

#include <type_traits>
#include <Eigen/Dense>

template <class Derived>
struct is_eigen : public std::is_base_of<Eigen::DenseBase<Derived>, Derived> {
};

我的第一反应是提供一个PrintTo的版本。不幸的是,编译器抱怨这个函数和Google-Test内部默认函数之间存在歧义。有没有办法消除歧义并将编译器指向我的函数?

namespace Eigen {                                                             
// This function will cause the following compiler error, when defined inside 
// the Eigen namespace.                                                       
//     gmock-1.7.0/gtest/include/gtest/gtest-printers.h:600:5: error:         
//          call to 'PrintTo' is ambiguous                                    
//        PrintTo(value, os);                                                 
//        ^~~~~~~                                                             
//                                                                            
// It will simply be ignore when defined in the global namespace.             
template <class Derived,                                                      
          class = typename std::enable_if<is_eigen<Derived>::value>::type>    
void PrintTo(const Derived &m, ::std::ostream *o) {                           
    *o << "\n" << m;                                                          
}                                                                             
}    

另一种方法是重载Eigen类型的operator<<。它确实起作用,但缺点是它是ostream运算符的全局重载。因此,无法定义任何特定于测试的格式(例如额外的换行符),而不会影响非测试代码。因此,我更喜欢像上面那个一样的专门的PrintTo方法。
template <class Derived,
          class = typename std::enable_if<is_eigen<Derived>::value>::type>
::std::ostream &operator<<(::std::ostream &o, const Derived &m) {
    o << "\n" << static_cast<const Eigen::DenseBase<Derived> &>(m);
    return o;
}

编辑:根据 @Alex 的答案

在下面的代码中,我实现了 @Alex 的解决方案,并实现了一个小函数,将 Eigen 矩阵的引用转换为可打印类型。

#include <Eigen/Dense>
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <gmock/gmock-matchers.h>

MATCHER_P(EigenEqual, expect,
          std::string(negation ? "isn't" : "is") + " equal to" +
              ::testing::PrintToString(expect)) {
    return arg == expect;
}

template <class Base>
class EigenPrintWrap : public Base {
    friend void PrintTo(const EigenPrintWrap &m, ::std::ostream *o) {
        *o << "\n" << m;
    }
};

template <class Base>
const EigenPrintWrap<Base> &print_wrap(const Base &base) {
    return static_cast<const EigenPrintWrap<Base> &>(base);
}

TEST(Eigen, Matrix) {
    Eigen::Matrix2i A, B;

    A << 1, 2,
         3, 4;
    B = A.transpose();

    EXPECT_THAT(print_wrap(A), EigenEqual(print_wrap(B)));
}

从一个空基类进行子类化,然后使用enable_if来检查您的类是否派生自该空基类,然后提供适当的重载,这个怎么样? - Alexander Oh
@Alex 谢谢你的评论。我不确定我是否正确理解了你的意思。你是说我应该将Eigen矩阵包装在另一个继承自空基类的类中吗?在这种情况下,我将完全控制该特定类,并且使用CRTP,我可以像使用Eigen插件一样添加PrintTo重载。但对我来说,包装器似乎是最后的选择。因为通常很麻烦确保它正确地复制接口。 - Lemming
2
为了补充@Lemming的最终解决方案,#define EXPECT_EIGEN_EQ(A, B) EXPECT_THAT(print_wrap(A), EigenEqual(print_wrap(B)));是有帮助的。 - user674155
4个回答

9
你遇到的问题是过载解析问题。
Google Test实现了一个模板函数。
namespace testing { namespace internal {

template <typename T>
void PrintTo(const T& value, std::ostream *o) { /* do smth */ }

} }

Eigen库定义了一个基于派生的打印函数。因此,
struct EigenBase { };
std::ostream& operator<< (std::ostream& stream, const EigenBase& m) { /* do smth */ }

struct Eigen : public EigenBase { };

void f1() {
  Eigen e;
  std::cout << e; // works
}

void f2() {
  Eigen e;
  print_to(eigen, &std::cout); // works
}

两者的设计都有问题。

Google Test不应该提供PrintTo的实现,而是应该在编译时检查用户是否提供了PrintTo,如果没有,则调用另一个默认打印函数PrintToDefault。根据重载分辨率,PrintTo提供的匹配更好。

另一方面,Eigen的operator<<基于派生,模板函数也会被重载分辨率优先选择。

Eigen可以提供一个CRTP基类,继承operator<<,这样类型匹配更好。

你可以从Eigen继承,并为你的继承类提供CRTP重载,避免这个问题。

#include <gtest/gtest.h>
#include <iostream>


class EigenBase {
};

std::ostream &operator<<(std::ostream &o, const EigenBase &r) {
    o << "operator<< EigenBase called";
    return o;
}

template <typename T>
void print_to(const T &t, std::ostream *o) {
    *o << "Google Print To Called";
}

class EigenSub : public EigenBase {};

template <typename T>
struct StreamBase {
    typedef T value_type;

    // friend function is inline and static
    friend std::ostream &operator<<(std::ostream &o, const value_type &r) {
        o << "operator<< from CRTP called";
        return o;
    }

    friend void print_to(const value_type &t, std::ostream *o) {
        *o << "print_to from CRTP called";

    }
};

// this is were the magic appears, because the oeprators are actually
// defined with signatures matching the MyEigenSub class.
class MyEigenSub : public EigenSub, public StreamBase<MyEigenSub> {
};

TEST(EigenBasePrint, t1) {
    EigenBase e;
    std::cout << e << std::endl; // works
}

TEST(EigenBasePrint, t2) {
    EigenBase e;
    print_to(e, &std::cout); // works
}

TEST(EigenSubPrint, t3) {
    EigenSub e;
    std::cout << e << std::endl; // works
}

TEST(EigenCRTPPrint, t4) {
    MyEigenSub e;
    std::cout << e << std::endl; // operator<< from CRTP called
}

TEST(EigenCRTPPrint, t5) {
    MyEigenSub e;
    print_to(e, &std::cout); // prints print_to from CRTP called
}

感谢您的回答。据我所知,这基本上与我使用EIGEN_MATRIXBASE_PLUGIN注入Eigen::MatrixBase的模式相同,但使用了外部包装类,对吗?看起来这将是那些不支持像Eigen提供的扩展方式的类的唯一选择。 - Lemming
我也在我的问题的最后添加了你的答案后的最终代码。以防万一... - Lemming

2

我感到有必要提供一个新的答案,我相信它比其他答案更简单更好,尽管它非常简单,但我可能会错过一些东西。它与您已经尝试过的解决方案非常相似,但并不完全相同。

本质上,您不必通过修改类的插件来跳跃。唯一的警告是,是的,您必须为每种类型(Matrix2dMatrix3d等)定义一个PrintTo函数;函数模板不起作用。但是,由于这是一个单元测试,我假设您知道所有类型,因此这不是问题。

所以本质上,只需将插件中的代码放入单元测试中,就像您尝试使用模板化的SFINAE启用器一样:

namespace Eigen
{
    void PrintTo(const Matrix2d &m, std::ostream *os)
    {
      *os << std::endl << m << std::endl;
    }
}

没有什么复杂的。这对我有用,根据您的测试用例和问题,应该可以满足您的需求。


1
这个答案基于问题结尾处的原始提问者的解决方案。然而,由于某些原因,在我的情况下PrintTo无法工作,我不得不实现operator<<
template <class Base>
class EigenPrintWrap : public Base
{
    friend std::ostream &operator<<(std::ostream &os, const EigenPrintWrap &m)
    {
        os << std::endl << static_cast<Base>(m) << std::endl;
        return os;
    }
};

template <class Base>
const EigenPrintWrap<Base> &print_wrap(const Base &base)
{
    return static_cast<const EigenPrintWrap<Base> &>(base);
}

请注意,转换为Base非常重要,否则operator<<将会无限递归。
然后我像这样使用它:
// helper for comparing Eigen types with ASSERT_PRED2
template <typename T>
inline bool is_approx(const T &lhs, const T &rhs)
{
    return lhs.isApprox(rhs, 1e-8);
}

// for more convenient use
#define ASSERT_MATRIX_ALMOST_EQUAL(m1, m2) \
    ASSERT_PRED2(is_approx<Eigen::MatrixXd>, print_wrap(m1), print_wrap(m2))

TEST(Eigen, Matrix) {
    Eigen::Matrix2i A, B;

    A << 1, 2,
         3, 4;
    B = A.transpose();

    ASSERT_MATRIX_ALMOST_EQUAL(A, B);
}

1
考虑到OP的答案,我想进行一些澄清。与OP派生的解决方案不同,我实际上想装饰类,而不是在断言中使用函数包装器。
为了简单起见,我重载了operator==,而不是使用google测试匹配谓词。
思路是,我们使用一个完全替代Eigen的包装器,而不是使用Eigen类本身。因此,每当我们创建一个Eigen实例时,我们创建一个WrapEigen实例。
由于我们不打算改变Eigen的实现,所以继承是可以的。
此外,我们希望向包装器添加功能。我在这里使用多个functor类的继承,命名为StreamBase和EqualBase。我们在这些functor中使用CRTP来获取正确的签名。
为了节省潜在的输入,我在Wrapper中使用了可变模板构造函数。如果存在相应的基础构造函数,则调用它。
工作示例:
#include <gtest/gtest.h>
#include <iostream>
#include <utility>

using namespace testing::internal;

struct EigenBase {
    explicit EigenBase(int i) : priv_(i) {}
    friend std::ostream &operator<<(std::ostream &o, const EigenBase &r) {
        o << r.priv_;
        return o;
    }
    friend bool operator==(const EigenBase& a, const EigenBase& b) {
        return a.priv_ == b.priv_;
    }
    int priv_;
};

struct Eigen : public EigenBase {
    explicit Eigen(int i) : EigenBase(i)  {}
};

template <typename T, typename U>
struct StreamBase {
    typedef T value_type;
    typedef const value_type &const_reference;

    friend void PrintTo(const value_type &t, std::ostream *o) {
        *o << static_cast<const U&>(t);
    }
};

template <typename T, typename U>
struct EqualBase {
    typedef T value_type;
    typedef const T &const_reference;

    friend bool operator==(const_reference a, const_reference b) {
        return static_cast<const U&>(a) 
            == static_cast<const U&>(b);
    }
};

template <typename T, typename U>
struct Wrapper 
    : public T,
      public StreamBase<Wrapper<T,U>, U>,
      public EqualBase<Wrapper<T,U>, U> {
    template <typename... Args>
    Wrapper(Args&&... args) : T(std::forward<Args>(args)...) { }
};

TEST(EigenPrint, t1) {
    Eigen e(10);
    Eigen f(11);
    ASSERT_EQ(e,f); // calls gtest::PrintTo
}

TEST(WrapEigenPrint, t1) {
    typedef Wrapper<Eigen, EigenBase> WrapEigen;
    WrapEigen e(10);
    WrapEigen f(11);
    ASSERT_EQ(e,f); // calls our own.
}

感谢澄清。现在,我想我理解您的意思了。这对于像boost:multi_array这样的类型应该很有效。不幸的是,当与Eigen类型一起使用时,它很容易被破坏。Eigen使用表达式模板,因此例如e + f是一个不同的类型,需要再次包装。这是我采用单独的包装器函数方法的动机。然而,两个版本都是兼容的。 - Lemming
@Lemming 我明白了,你的方法更合适,除非你想包装整个公共接口。 - Alexander Oh
1
看起来是这样。包装整个公共接口有点过度,因为我只需要在测试中使用它。 - Lemming

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