21

介绍

我正在使用 Google 的测试框架 Google-Mock 对特征矩阵编写测试,正如另一个问题中已经讨论的那样。

使用以下代码,我能够添加自定义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);
}

这样做是通过他们的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 打印矩阵的十六进制转储,而不是更好地表示它们的值。谷歌文档对自定义类型的打印值进行了以下说明:

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

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

template<typename Derived>
std::ostream &operator<< (std::ostream &s, const DenseBase<Derived> &m);

即它需要一个const DenseBase<Derived>&. 另一方面,Google-test hex-dump 默认打印机是模板函数的默认实现。你可以在这里找到实现。(跟随从PrintTo开始的调用树,看看情况是否如此,或者证明我错了。;))

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


我的问题

我的问题如下。我如何告诉编译器更喜欢 Eigen 特定operator <<于 Google-test hex-dump?假设我不能修改 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 那样的插件机制,并且我无法直接访问其定义。

因此,我的问题是:有没有办法在不修改类定义本身的情况下将编译器指向正确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 类型定义了一个特征。有了它,我们可以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;                                                          
}                                                                             
}    

另一种方法是重载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;
}

编辑:关注@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)));
}
4

3 回答 3

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内容比您提供的内容更好(根据重载决议)。

另一方面,Eigenoperator<<是基于派生的,并且重载决议也将首选模板函数。

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
}
于 2014-08-12T13:09:41.517 回答
1

考虑到 OP 的回答,我想做一些澄清。与来自 OP 的派生解决方案不同,我实际上想装饰类而不是在断言中使用函数包装器。

为了简单起见,而不是使用谷歌测试匹配谓词,我重载了operator==.

理念

我们不使用 Eigen 类本身,而是使用完全替代 Eigen 的包装器。因此,每当我们创建一个实例时,我们都会创建一个Eigen实例WrapEigen

因为我们不打算改变Eigen派生的实现是好的。

此外,我们想向包装器添加功能。我在这里使用仿函数的多重继承来执行此操作,例如名为StreamBaseand的类EqualBase。我们在这些函子中使用 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.
}
于 2014-08-26T10:52:47.333 回答
0

我觉得有必要提供一个我认为比其他答案更简单和更好的新答案,尽管它是如此简单以至于我可能错过了一些东西。它与您已经尝试过的解决方案非常相似,但并不完全相同。

本质上,您不必跳过修改类的插件圈。需要注意的是,是的,您必须PrintTo为每种类型(Matrix2d,,Matrix3d等)定义一个函数;功能模板不起作用。但由于这是一个单元测试,我假设你知道你所有的类型是什么,所以这不是问题。

因此,基本上从插件中获取您的代码,然后将其放入单元测试中,就像您尝试使用支持 SFINAE 的模板化代码一样:

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

没有什么花哨。这对我有用,应该根据你的测试用例和问题做你想做的事。

于 2018-05-23T07:56:08.063 回答