介绍
我正在使用谷歌的测试框架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));
}
这个测试会失败,因为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>
如您所见,Google-Test打印的是矩阵的十六进制转储,而不是更好的值表示方式。 Google文档对于自定义类型的值打印说:
Eigen矩阵附带一个该打印机知道如何打印内置C++类型、本地数组、STL容器和支持<<运算符的任何类型。对于其他类型,它会打印值中的原始字节,并希望您用户能够弄清楚。
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 OhPrintTo
重载。但对我来说,包装器似乎是最后的选择。因为通常很麻烦确保它正确地复制接口。 - Lemming#define EXPECT_EIGEN_EQ(A, B) EXPECT_THAT(print_wrap(A), EigenEqual(print_wrap(B)));
是有帮助的。 - user674155