如何在C ++中比较泛型结构?


13

我想以一种通用的方式比较结构,并且我做了这样的事情(我无法共享实际的源代码,因此如有必要,请提供更多详细信息):

template<typename Data>
bool structCmp(Data data1, Data data2)
{
  void* dataStart1 = (std::uint8_t*)&data1;
  void* dataStart2 = (std::uint8_t*)&data2;
  return memcmp(dataStart1, dataStart2, sizeof(Data)) == 0;
}

这通常可以按预期工作,但有时即使两个结构实例具有相同的成员(我已经使用eclipse调试器进行了检查),它有时仍返回false。经过一些搜索,我发现memcmp由于填充了所使用的结构而导致失败。

有没有比较合适的内存填充方式?我无法修改所使用的结构(它们是我正在使用的API的一部分),并且所使用的许多不同结构具有一些不同的成员,因此无法以一般方式进行单独比较(据我所知)。

编辑:不幸的是,我坚持使用C ++ 11。应该早些提到...


您可以举一个失败的例子吗?对于一种类型的所有实例,填充都应该相同,不是吗?
idclev 463035818

1
@ idclev463035818填充没有指定,您不能假设它的价值,我认为尝试阅读它是UB(不确定最后一部分)。
弗朗索瓦·安德里厄

@ idclev463035818填充在内存中的相对位置相同,但可以具有不同的数据。在正常使用该结构时会将其丢弃,因此编译器可能不会将其归零。
NO_NAME,

2
@ idclev463035818填充具有相同的大小。构成填充的位的状态可以是任何东西。当您memcmp在比较中包括那些填充位时。
弗朗索瓦·安德里厄

1
我同意Yksisarvinen ...使用类而不是结构,并实现==运算符。使用memcmp是不可靠的,并且迟早您将要处理某个必须“与其他类稍有不同”的类。在操作员中实现它非常干净和高效。实际的行为将是多态的,但是源代码将是干净的……而且显而易见。
麦克鲁滨逊

Answers:


7

不,memcmp不适合这样做。在这一点上,C ++中的反射还不足以做到这一点(将会有实验性的编译器支持足够强大的反射功能来执行此操作,而可能具有所需的功能)。

如果没有内置反射,解决问题的最简单方法就是进行一些手动反射。

拿着它:

struct some_struct {
  int x;
  double d1, d2;
  char c;
};

我们希望进行最少的工作,因此我们可以比较其中的两个。

如果我们有:

auto as_tie(some_struct const& s){ 
  return std::tie( s.x, s.d1, s.d2, s.c );
}

要么

auto as_tie(some_struct const& s)
-> decltype(std::tie( s.x, s.d1, s.d2, s.c ))
{
  return std::tie( s.x, s.d1, s.d2, s.c );
}

对于,则:

template<class S>
bool are_equal( S const& lhs, S const& rhs ) {
  return as_tie(lhs) == as_tie(rhs);
}

做得不错。

我们可以通过一些工作来将此过程扩展为递归的。而不是比较关系,而是比较包装在模板中的每个元素,并且该模板将operator==递归地应用此规则(将元素包装as_tie以进行比较),除非该元素已经具有work ==并处理数组。

这将需要一些库(100行代码?)以及编写一些手动的每个成员的“反射”数据。如果您拥有的结构数量有限,则手动编写每个结构的代码可能会更容易。


可能有办法

REFLECT( some_struct, x, d1, d2, c )

as_tie使用可怕的宏来生成结构。但是as_tie很简单。在,重复很烦人。这很有用:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

在这种情况下,还有许多其他情况。使用RETURNS,写作as_tie是:

auto as_tie(some_struct const& s)
  RETURNS( std::tie( s.x, s.d1, s.d2, s.c ) )

删除重复。


这是递归的一个障碍:

template<class T,
  typename std::enable_if< !std::is_class<T>{}, bool>::type = true
>
auto refl_tie( T const& t )
  RETURNS(std::tie(t))

template<class...Ts,
  typename std::enable_if< (sizeof...(Ts) > 1), bool>::type = true
>
auto refl_tie( Ts const&... ts )
  RETURNS(std::make_tuple(refl_tie(ts)...))

template<class T, std::size_t N>
auto refl_tie( T const(&t)[N] ) {
  // lots of work in C++11 to support this case, todo.
  // in C++17 I could just make a tie of each of the N elements of the array?

  // in C++11 I might write a custom struct that supports an array
  // reference/pointer of fixed size and implements =, ==, !=, <, etc.
}

struct foo {
  int x;
};
struct bar {
  foo f1, f2;
};
auto refl_tie( foo const& s )
  RETURNS( refl_tie( s.x ) )
auto refl_tie( bar const& s )
  RETURNS( refl_tie( s.f1, s.f2 ) )

refl_tie(array)(完全递归,甚至支持数组数组):

template<class T, std::size_t N, std::size_t...Is>
auto array_refl( T const(&t)[N], std::index_sequence<Is...> )
  RETURNS( std::array<decltype( refl_tie(t[0]) ), N>{ refl_tie( t[Is] )... } )

template<class T, std::size_t N>
auto refl_tie( T(&t)[N] )
  RETURNS( array_refl( t, std::make_index_sequence<N>{} ) )

现场例子

在这里我使用std::arrayrefl_tie。这比我在编译时以前的refl_tie元组要快得多。

template<class T,
  typename std::enable_if< !std::is_class<T>{}, bool>::type = true
>
auto refl_tie( T const& t )
  RETURNS(std::cref(t))

使用std::cref此处而不是std::tie可以节省编译时的开销,因为这cref是比类更简单的类tuple

最后,您应该添加

template<class T, std::size_t N, class...Ts>
auto refl_tie( T(&t)[N], Ts&&... ) = delete;

这将防止数组成员退化为指针,而后退到指针相等(您可能不需要数组)。

没有这个,如果您将数组传递给未反射的结构,它就会退回到指针指向未反射的结构上refl_tie,该结构可以工作并返回无意义。

这样,您最终会遇到编译时错误。


通过库类型支持递归比较棘手。你可以std::tie他们:

template<class T, class A>
auto refl_tie( std::vector<T, A> const& v )
  RETURNS( std::tie(v) )

但这不支持通过它进行递归。


我想通过人工思考来寻求这种解决方案。您提供的代码似乎不适用于C ++ 11。您有什么机会可以帮助我吗?
Fredrik Enetorp

1
这在C ++ 11中不起作用的原因是缺少尾随返回类型as_tie。从C ++ 14开始,这是自动推断的。您可以auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));在C ++ 11中使用。或明确声明返回类型。
Darhuuk

1
@FredrikEnetorp固定,再加上一个易于编写的宏。使它完全递归地工作的工作(因此,子结构获得as_tie支持的结构化结构会自动运行)和支持数组成员的工作没有详述,但有可能。
Yakk-亚当·内夫罗蒙特

谢谢。我对恐怖宏的处理略有不同,但是在功能上是等效的。只是一个问题。我正在尝试将比较归纳为一个单独的头文件,并将其包含在各种gmock测试文件中。这导致出现错误消息:as_tie(Test1 const&)的多个定义我试图内联它们,但无法使其正常工作。
Fredrik Enetorp

1
@FredrikEnetorp inline关键字应消除多个定义错误。在得到一个最小的可复制示例
Yakk-Adam Nevraumont

7

没错,填充会妨碍您以这种方式比较任意类型。

您可以采取以下措施:

  • 如果您可以控制,Data则例如gcc has __attribute__((packed))。它对性能有影响,但是可能值得尝试一下。虽然,我不得不承认我不知道是否packed能使您完全禁止填充。Gcc文件说:

附加到struct或union类型定义的此属性指定将结构或union的每个成员放置在最小化所需内存的位置。当附加到枚举定义时,它指示应使用最小的整数类型。

如果T是TriviallyCopyable,并且具有相同值的T类型的任何两个对象具有相同的对象表示,则提供等于true的成员常量值。对于任何其他类型,值均为false。

并进一步:

引入此特征可以通过将其对象表示形式哈希为字节数组来确定类型是否可以正确哈希。

PS:我只写给填充,但不要忘记的类型,可以比较相等与内存中的不同表现情况决非罕见的(例如std::stringstd::vector和许多其他)。


1
我喜欢这个答案。有了这种类型特征,您可以使用SFINAE memcmp在没有填充的结构上使用,并且operator==仅在需要时才实现。
Yksisarvinen

好,谢谢。有了这个,我可以安全地得出结论,我需要进行一些手动思考。
Fredrik Enetorp

6

简而言之:不可能以通用的方式。

问题memcmp在于填充可能包含任意数据,因此memcmp可能会失败。如果有一种方法可以找出填充的位置,则可以将这些位清零,然后比较数据表示形式,这将检查成员之间的可比性是否相等(不是这种情况,std::string因为两个字符串可以包含不同的指针,但指向的两个字符数组相等)。但是我不知道如何填充结构。您可以尝试告诉编译器打包这些结构,但这会使访问速度变慢,并且实际上并不能保证正常工作。

实现此目的的最干净的方法是比较所有成员。当然,这实际上不是通用的方法(直到我们在C ++ 23或更高版本中获得编译时反射和元类)。从C ++ 20开始,可以生成一个默认值,operator<=>但我认为这也只能作为成员函数,因此,这实际上并不适用。如果您很幸运,并且要比较的所有结构都有operator==定义,则当然可以使用它。但这并不能保证。

编辑:好的,实际上有一种完全hacky的聚合方式。(我只将转换写为元组,那些具有默认的比较运算符)。哥德宝


不错!不幸的是,我坚持使用C ++ 11,因此无法使用它。
Fredrik Enetorp

2

C ++ 20支持默认兼容性

#include <iostream>
#include <compare>

struct XYZ
{
    int x;
    char y;
    long z;

    auto operator<=>(const XYZ&) const = default;
};

int main()
{
    XYZ obj1 = {4,5,6};
    XYZ obj2 = {4,5,6};

    if (obj1 == obj2)
    {
        std::cout << "objects are identical\n";
    }
    else
    {
        std::cout << "objects are not identical\n";
    }
    return 0;
}

1
尽管这是一个非常有用的功能,但它无法回答所要求的问题。OP确实说“我无法修改所使用的结构”,这意味着即使C ++ 20默认相等运算符可用,OP也将无法使用它们,因为只能将==or <=>运算符默认为在班级范围内。
Nicol Bolas

就像Nicol Bolas所说的那样,我无法修改结构。
Fredrik Enetorp

1

假定POD数据,默认分配运算符仅复制成员字节。(实际上对此不是100%的确定,请不要相信我)

您可以利用此优势:

template<typename Data>
bool structCmp(Data data1, Data data2) // Data is POD
{
  Data tmp;
  memcpy(&tmp, &data1, sizeof(Data)); // copy data1 including padding
  tmp = data2;                        // copy data2 only members
  return memcmp(&tmp, &data1, sizeof(Data)) == 0; 
}

@walnut你是对的,这是一个糟糕的答案。改写一个。
科斯塔斯

标准是否保证分配使填充字节保持不变?对于基本类型中相同值的多个对象表示形式,仍然存在担忧。
胡桃

@walnut我相信是的
科斯塔斯

1
该链接的最高答案下面的注释似乎表明没有。答案本身只是说填充不必被复制,但不是它musn't。我也不知道。
胡桃

我现在已经对其进行了测试,但是它不起作用。分配不会使填充字节保持不变。
Fredrik Enetorp

0

我相信您也许可以在库中使用Antony Polukhin奇妙而狡猾的伏都教为基础的解决方案magic_get-适用于结构,而不适用于复杂的类。

使用该库,我们可以在纯通用模板的代码中迭代结构的不同字段及其适当的类型。举例来说,Antony使用它来能够将任意结构流式传输到具有正确类型的输出流(完全通用)。有理由认为,比较也可能是这种方法的一种可能的应用。

...但是您将需要C ++ 14。至少它比C ++ 17和其他答案中的后续建议要好:-P

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.