教Google-Test如何打印Eigen Matrix

Lem*_*ing 17 c++ templates googletest eigen gmock

介绍

我正在使用Google的测试框架Google-Mock编写关于Eigen矩阵的测试,正如另一个问题中已经讨论的那样.

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

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);
}
Run Code Online (Sandbox Code Playgroud)

这样做是通过他们的isApprox方法比较两个特征矩阵,如果它们不匹配,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));
}
Run Code Online (Sandbox Code Playgroud)

此测试将失败,因为A并且B不相等.不幸的是,相应的错误消息如下所示:

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>
Run Code Online (Sandbox Code Playgroud)

如您所见,Google-Test打印矩阵的十六进制转储,而不是更好地表示其值.在谷歌的文档说,关于打印自定义类型的值如下:

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

特征矩阵带有一个operator<<.但是,Google-Test或C++编译器忽略了它.据我所知,由于以下原因:该运算符的签名为(IO.h(第240行))

template<typename Derived>
std::ostream &operator<< (std::ostream &s, const DenseBase<Derived> &m);
Run Code Online (Sandbox Code Playgroud)

即它需要一个const DenseBase<Derived>&.另一方面,Google测试hex-dump默认打印机是模板功能的默认实现.你可以在这里找到实现.(按照从PrintTo开始的调用树来查看是这种情况,或者证明我错了.;))

因此,Google-Test默认打印机是一个更好的匹配,因为它需要一个const Derived &,而不仅仅是它的基类const DenseBase<Derived> &.


我的问题

我的问题如下.如何告诉编译器更喜欢特定operator <<于Google测试的十六进制转储?假设我无法修改特征矩阵的类定义.


我的尝试

到目前为止,我已经尝试了以下几点.

定义一个函数

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

不会因为不起作用的相同原因operator<<而起作用.

我发现唯一有用的是使用Eigen的插件机制.

用文件eigen_matrix_addons.hpp:

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

以及include指令

#define EIGEN_MATRIXBASE_PLUGIN "eigen_matrix_addons.hpp"
#include <Eigen/Dense>
Run Code Online (Sandbox Code Playgroud)

测试将产生以下输出:

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
Run Code Online (Sandbox Code Playgroud)

这有什么问题?

对于特征矩阵,这可能是一个可接受的解决方案.但是,我知道我必须很快将相同的东西应用到其他模板类中,遗憾的是,它不提供像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));
}
Run Code Online (Sandbox Code Playgroud)

编辑:进一步尝试

我在SFINAE方法上取得了一些进展.

首先,我为Eigen类型定义了一个特征.有了它,我们可以使用std::enable_if仅为满足此特征的类型提供模板功能.

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

template <class Derived>
struct is_eigen : public std::is_base_of<Eigen::DenseBase<Derived>, Derived> {
};
Run Code Online (Sandbox Code Playgroud)

我的第一个想法是提供这样一个版本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;                                                          
}                                                                             
}    
Run Code Online (Sandbox Code Playgroud)

另一种方法是重载operator<<Eigen类型.它确实有效.然而,缺点是它是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;
}
Run Code Online (Sandbox Code Playgroud)

编辑:关注@ Alex的答案

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

#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)));
}
Run Code Online (Sandbox Code Playgroud)

Ale*_*lex 6

遇到的问题是重载解决问题.

谷歌测试实现了模板功能

namespace testing { namespace internal {

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

} }
Run Code Online (Sandbox Code Playgroud)

特征库定义了基于推导的打印机功能.于是

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
}
Run Code Online (Sandbox Code Playgroud)

两者都有可疑的设计.

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
}
Run Code Online (Sandbox Code Playgroud)