Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compare generic structs in C++?

I want to compare structs in a generic way and I've done something like this (I cannot share the actual source, so ask for more details if necessary):

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

This mostly works as intended, except sometimes it returns false even though the two struct instances have identical members (I've checked with eclipse debugger). After some searching I discovered that memcmp can fail due to the struct used being padded.

Is there a more proper way of comparing memory that's indifferent to padding? I'm not able to modify the structs used (they're part of an API I'm using) and the many different structs used has some differing members and thus cannot be compared individually in a generic way (to my knowledge).

Edit: I'm unfortunately stuck with C++11. Should've mentioned this earlier...

like image 706
Fredrik Enetorp Avatar asked Feb 05 '20 16:02

Fredrik Enetorp


People also ask

Can you compare two structs in C?

To find out if they are the same object, compare pointers to the two structs for equality. If you want to find out in general if they have the same value you have to do a deep comparison. This involves comparing all the members. If the members are pointers to other structs you need to recurse into those structs too.

Can we compare structs with functions?

You can't compare structures with == (nor should you use memcmp ). The best way is to write a custom comparison function that compares two struct objects of the given type.


1 Answers

No, memcmp is not suitable to do this. And reflection in C++ is insufficient to do this at this point (there are going to be experimental compilers that support reflection strong enough to do this already, and c++23 might have the features you need).

Without built-in reflection, the easiest way to solve your problem is to do some manual reflection.

Take this:

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

we want to do the minimal amount of work so we can compare two of these.

If we have:

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

or

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

for c++11, then:

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

does a pretty decent job.

We can extend this process to be recursive with a bit of work; instead of comparing ties, compare each element wrapped in a template, and that template's operator== recursively applies this rule (wrapping the element in as_tie to compare) unless the element already has a working ==, and handles arrays.

This will require a bit of a library (100ish lines of code?) together with writing a bit of manual per-member "reflection" data. If the number of structs you have is limited, it might be easier to write per-struct code manually.


There are probably ways to get

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

to generate the as_tie structure using horrible macros. But as_tie is simple enough. In c++11 the repetition is annoying; this is useful:

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

in this situation and many others. With RETURNS, writing as_tie is:

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

removing the repetition.


Here is a stab at making it recursive:

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

c++17 refl_tie(array) (fully recursive, even supports 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>{} ) )

Live example.

Here I use a std::array of refl_tie. This is much faster than my previous tuple of refl_tie at compile time.

Also

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

using std::cref here instead of std::tie could save on compile-time overhead, as cref is a much simpler class than tuple.

Finally, you should add

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

which will prevent array members from decaying to pointers and falling back on pointer-equality (which you probably don't want from arrays).

Without this, if you pass an array to a non-reflected struct in, it falls back on pointer-to-non-reflected struct refl_tie, which works and returns nonsense.

With this, you end up with a compile-time error.


Support for recursion through library types is tricky. You could std::tie them:

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

but that doesn't support recursion through it.

like image 195
Yakk - Adam Nevraumont Avatar answered Sep 22 '22 22:09

Yakk - Adam Nevraumont