14

我想以一种通用的方式比较结构,我做了这样的事情(我不能分享实际的来源,所以如果需要,请询问更多细节):

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

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

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

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

4

6 回答 6

7

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

您可以采取以下措施:

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

此属性附加到结构或联合类型定义,指定结构或联合的每个成员的放置以最小化所需的内存。当附加到枚举定义时,它表明应该使用最小的整数类型。

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

并进一步:

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

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

于 2020-02-05T16:38:51.620 回答
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进行比较),除非该元素已经有一个工作==,并处理数组。

这将需要一些库(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) (完全递归,甚至支持arrays-of-arrays):

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::arrayof refl_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) )

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

于 2020-02-05T16:46:43.883 回答
6

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

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

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

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

于 2020-02-05T16:38:47.207 回答
3

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;
}
于 2020-02-05T17:06:18.923 回答
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; 
}
于 2020-02-05T16:45:14.917 回答
0

我相信您可以在库中基于 Antony Polukhin 的奇妙狡猾巫毒教的解决方案magic_get- 用于结构,而不是复杂类。

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

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

于 2020-02-28T22:50:59.003 回答