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

How to compare generic structs in C++?

我想以一种通用的方式比较结构,我已经做了类似的事情(我无法分享实际来源,所以如有必要请询问更多细节):

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;
}

这主要按预期工作,除了有时它 returns false 即使两个结构实例具有相同的成员(我已经用 eclipse 调试器检查过)。经过一番搜索后,我发现 memcmp 可能会因为使用的结构被填充而失败。

是否有更合适的方法来比较与填充无关的内存?我无法修改使用的结构(它们是我正在使用的 API 的一部分)并且使用的许多不同结构有一些不同的成员,因此不能以通用方式单独比较(对我知识)。

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

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

memcmp 的问题是填充可能包含任意数据,因此 memcmp 可能会失败。如果有办法找出填充的位置,您可以将这些位归零,然后比较数据表示,如果成员是平凡可比的(情况并非如此,即 std::string 因为两个字符串可以包含不同的指针,但指向的两个字符数组是相等的)。但我知道没有办法获得结构的填充。您可以尝试告诉您的编译器打包结构,但这会使访问变慢并且不能真正保证工作。

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

编辑: 好吧,实际上有一个完全 hacky 和有点通用的聚合方式。 (我只写了到元组的转换,那些有一个默认的比较运算符)。 godbolt

你是对的,填充会妨碍你以这种方式比较任意类型。

您可以采取以下措施:

  • 如果您控制 Data 那么例如 gcc 有 __attribute__((packed))。它会影响性能,但值得一试。不过,我不得不承认我不知道 packed 是否能让您完全禁止填充。 Gcc doc 说:

This attribute, attached to struct or union type definition, specifies that each member of the structure or union is placed to minimize the memory required. When attached to an enum definition, it indicates that the smallest integral type should be used.

If T is TriviallyCopyable and if any two objects of type T with the same value have the same object representation, provides the member constant value equal true. For any other type, value is false.

还有:

This trait was introduced to make it possible to determine whether a type can be correctly hashed by hashing its object representation as a byte array.

PS:我只解决了填充问题,但不要忘记对于内存中具有不同表示的实例可以比较相等的类型绝非罕见(例如 std::stringstd::vector和许多其他人)。

假设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; 
}

不,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 中进行比较)除非该元素已经有一个有效的 == ,并处理数组。

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


大概有办法得到

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

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

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

在这种情况和许多其他情况下。用RETURNSas_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(数组)(完全递归,甚至支持数组的数组):

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>{} ) )

Live example.

这里我用的是refl_tiestd::array。这比我之前编译时的 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 可以节省编译时开销,因为 creftuple 更简单 class。

最后,你应该添加

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

这将防止数组成员衰减为指针并退回到指针相等(您可能不希望数组中出现这种情况)。

如果没有这个,如果你将一个数组传递给一个非反射结构,它会返回到指向非反射结构的指针 refl_tie,这有效并且 returns 废话。

有了这个,你最终会遇到编译时错误。


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

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

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

C++ 20 支持default comaparisons

#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;
}

我相信您可以根据 magic_get 库中 Antony Polukhin 奇妙狡猾的伏都教的解决方案 - 对于结构,而不是复杂 类。

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

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