Wie vergleiche ich generische Strukturen in C ++?

13

Ich möchte Strukturen generisch vergleichen und habe so etwas getan (ich kann die tatsächliche Quelle nicht teilen, bitte fragen Sie bei Bedarf nach weiteren Details):

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

Dies funktioniert meistens wie beabsichtigt, außer dass manchmal false zurückgegeben wird, obwohl die beiden Strukturinstanzen identische Mitglieder haben (ich habe dies mit dem Eclipse-Debugger überprüft). Nach einiger Suche stellte ich fest, dass memcmpdies aufgrund der aufgefüllten Struktur fehlschlagen kann.

Gibt es eine geeignetere Methode zum Vergleichen des Speichers, die dem Auffüllen gleichgültig ist? Ich kann die verwendeten Strukturen nicht ändern (sie sind Teil einer von mir verwendeten API) und die vielen verschiedenen verwendeten Strukturen haben einige unterschiedliche Mitglieder und können daher meines Wissens nicht generisch einzeln verglichen werden.

Edit: Ich bin leider mit C ++ 11 stecken. Hätte das früher erwähnen sollen ...

Fredrik Enetorp
quelle
Können Sie ein Beispiel zeigen, bei dem dies fehlschlägt? Die Polsterung sollte für alle Instanzen eines Typs gleich sein, nicht wahr?
idclev 463035818
1
@ idclev463035818 Padding ist nicht spezifiziert, Sie können nicht davon ausgehen, dass es ein Wert ist, und ich glaube, es ist UB, zu versuchen, es zu lesen (nicht sicher in diesem letzten Teil).
François Andrieux
@ idclev463035818 Das Auffüllen befindet sich an denselben relativen Stellen im Speicher, kann jedoch unterschiedliche Daten enthalten. Es wird bei normalen Verwendungen der Struktur verworfen, sodass der Compiler sich möglicherweise nicht die Mühe macht, es auf Null zu setzen.
NO_NAME
2
@ idclev463035818 Die Polsterung hat die gleiche Größe. Der Zustand der Bits, aus denen diese Auffüllung besteht, kann beliebig sein. Wenn Sie memcmpdiese Füllbits in Ihren Vergleich einbeziehen.
François Andrieux
1
Ich stimme Yksisarvinen zu ... benutze Klassen, keine Strukturen, und implementiere den ==Operator. Die Verwendung memcmpist unzuverlässig, und früher oder später werden Sie es mit einer Klasse zu tun haben, die "es ein wenig anders machen muss als die anderen". Es ist sehr sauber und effizient, dies in einem Bediener zu implementieren. Das tatsächliche Verhalten ist polymorph, aber der Quellcode ist sauber ... und offensichtlich.
Mike Robinson

Antworten:

7

Nein, memcmpist dazu nicht geeignet. Und die Reflexion in C ++ reicht zu diesem Zeitpunkt nicht aus, um dies zu tun (es wird experimentelle Compiler geben, die die Reflexion unterstützen, die stark genug ist, um dies bereits zu tun, und möglicherweise über die Funktionen, die Sie benötigen).

Ohne eingebaute Reflexion können Sie Ihr Problem am einfachsten durch manuelle Reflexion lösen.

Nimm das:

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

Wir wollen den minimalen Arbeitsaufwand erledigen, damit wir zwei davon vergleichen können.

Wenn wir haben:

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

oder

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

für dann:

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

macht einen ziemlich anständigen Job.

Wir können diesen Prozess mit ein wenig Arbeit so erweitern, dass er rekursiv ist. Anstatt Bindungen zu vergleichen, vergleichen Sie jedes Element, das in eine Vorlage eingeschlossen ist, und diese Vorlage operator==wendet diese Regel rekursiv an (das Element as_tiezum Vergleich einschließen), es sei denn, das Element verfügt bereits über eine Funktion ==und verarbeitet Arrays.

Dies erfordert ein bisschen Bibliothek (100 Codezeilen?) Zusammen mit dem Schreiben von manuellen "Reflexions" -Daten pro Mitglied. Wenn die Anzahl Ihrer Strukturen begrenzt ist, ist es möglicherweise einfacher, Code pro Struktur manuell zu schreiben.


Es gibt wahrscheinlich Möglichkeiten zu bekommen

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

die as_tieStruktur mit schrecklichen Makros zu generieren . Ist as_tieaber einfach genug. In die Wiederholung ärgerlich; das ist nützlich:

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

in dieser Situation und vielen anderen. Mit RETURNSSchreiben as_tieist:

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

Entfernen der Wiederholung.


Hier ist ein Versuch, es rekursiv zu machen:

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) (vollständig rekursiv, unterstützt sogar Arrays von 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 Beispiel .

Hier benutze ich ein std::arrayvon refl_tie. Dies ist viel schneller als mein vorheriges Tupel von refl_tie zur Kompilierungszeit.

Ebenfalls

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

Wenn Sie std::crefhier anstelle von std::tieverwenden, können Sie Overhead beim Kompilieren sparen, da dies crefeine viel einfachere Klasse ist als tuple.

Schließlich sollten Sie hinzufügen

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

Dies verhindert, dass Array-Mitglieder in Zeiger zerfallen und auf die Zeigergleichheit zurückgreifen (was Sie von Arrays wahrscheinlich nicht wollen).

Ohne dies wird, wenn Sie ein Array an eine nicht reflektierte Struktur in übergeben, auf eine Zeiger-zu-nicht-reflektierte Struktur zurückgegriffen refl_tie, die funktioniert und Unsinn zurückgibt.

Dies führt zu einem Fehler bei der Kompilierung.


Die Unterstützung der Rekursion durch Bibliothekstypen ist schwierig. Sie könnten std::tiesie:

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

aber das unterstützt keine Rekursion.

Yakk - Adam Nevraumont
quelle
Ich möchte diese Art von Lösung mit manuellen Reflexionen verfolgen. Der von Ihnen angegebene Code scheint nicht mit C ++ 11 zu funktionieren. Gibt es eine Chance, dass du mir dabei helfen kannst?
Fredrik Enetorp
1
Der Grund, warum dies in C ++ 11 nicht funktioniert, ist das Fehlen eines nachgestellten Rückgabetyps as_tie. Ab C ++ 14 wird dies automatisch abgeleitet. Sie können auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));in C ++ 11 verwenden. Oder geben Sie den Rückgabetyp explizit an.
Darhuuk
1
@FredrikEnetorp Behoben, plus ein Makro, das das Schreiben erleichtert. Die Arbeit, um es vollständig rekursiv zum Laufen zu bringen (eine Struktur-von-Struktur, bei der die Unterstrukturen as_tieUnterstützung haben, funktioniert automatisch) und die Unterstützung von Array-Mitgliedern, ist nicht detailliert, aber möglich.
Yakk - Adam Nevraumont
Vielen Dank. Ich habe die schrecklichen Makros etwas anders gemacht, aber funktional gleichwertig. Nur noch ein Problem. Ich versuche, den Vergleich in einer separaten Header-Datei zu verallgemeinern und in verschiedene gmock-Testdateien aufzunehmen. Dies führt zu der Fehlermeldung: Mehrfachdefinition von `as_tie (Test1 const &) 'Ich versuche sie zu inline, kann sie aber nicht zum Laufen bringen.
Fredrik Enetorp
1
@FredrikEnetorp Das inlineSchlüsselwort sollte dazu führen, dass mehrere Definitionsfehler verschwinden . Verwenden Sie die Schaltfläche [Frage stellen], nachdem Sie ein minimal reproduzierbares Beispiel erhalten haben
Yakk - Adam Nevraumont
7

Sie haben Recht, dass das Auffüllen den Vergleich beliebiger Typen auf diese Weise behindert.

Es gibt Maßnahmen, die Sie ergreifen können:

  • Wenn Sie die Kontrolle haben, Datadann hat zB gcc __attribute__((packed)). Es hat Auswirkungen auf die Leistung, aber es könnte sich lohnen, es auszuprobieren. Allerdings muss ich zugeben, dass ich nicht weiß, ob packedSie die Polsterung vollständig verbieten können. Gcc doc sagt:

Dieses Attribut, das an die Struktur- oder Unionstypdefinition angehängt ist, gibt an, dass jedes Mitglied der Struktur oder Union platziert wird, um den erforderlichen Speicher zu minimieren. Wenn es an eine Aufzählungsdefinition angehängt wird, gibt es an, dass der kleinste Integraltyp verwendet werden sollte.

Wenn T TriviallyCopyable ist und zwei Objekte vom Typ T mit demselben Wert dieselbe Objektdarstellung haben, wird der Elementkonstantenwert gleich true angegeben. Für jeden anderen Typ ist der Wert false.

und weiter:

Dieses Merkmal wurde eingeführt, um zu bestimmen, ob ein Typ korrekt gehasht werden kann, indem seine Objektdarstellung als Bytearray gehasht wird.

PS: Ich habe nur adressierte Polsterung, aber nicht , dass Typen vergessen , das für Instanzen mit unterschiedlicher Darstellung im Speicher ist keineswegs selten (zB gleich vergleichen std::string, std::vectorund viele andere).

idclev 463035818
quelle
1
Ich mag diese Antwort. Mit diesem Typmerkmal können Sie SFINAE verwenden, um memcmpStrukturen ohne Auffüllung zu verwenden und operator==nur bei Bedarf zu implementieren .
Yksisarvinen
OK danke. Damit kann ich sicher schließen, dass ich einige manuelle Überlegungen anstellen muss.
Fredrik Enetorp
6

Kurzum: Generisch nicht möglich.

Das Problem dabei memcmpist, dass das Auffüllen beliebige Daten enthalten kann und daher memcmpmöglicherweise fehlschlägt. Wenn es eine Möglichkeit gäbe, herauszufinden, wo sich die Auffüllung befindet, könnten Sie diese Bits auf Null setzen und dann die Datendarstellungen vergleichen, um die Gleichheit zu überprüfen, wenn die Elemente trivial vergleichbar sind (was nicht der Fall ist, dh std::stringda zwei Zeichenfolgen dies können enthalten unterschiedliche Zeiger, aber die beiden spitzen Zeichen-Arrays sind gleich). Aber ich kenne keine Möglichkeit, an das Auffüllen von Strukturen heranzukommen. Sie können versuchen, Ihren Compiler anzuweisen, die Strukturen zu packen. Dies verlangsamt jedoch den Zugriff und ist nicht wirklich garantiert.

Der sauberste Weg, dies zu implementieren, besteht darin, alle Mitglieder zu vergleichen. Natürlich ist dies nicht generisch möglich (bis wir Kompilierungszeitreflexionen und Metaklassen in C ++ 23 oder höher erhalten). Ab C ++ 20 könnte man einen Standard generieren, operator<=>aber ich denke, dies wäre auch nur als Mitgliedsfunktion möglich, so dass dies wiederum nicht wirklich anwendbar ist. Wenn Sie Glück haben und alle Strukturen, die Sie vergleichen möchten, eine operator==definierte haben, können Sie diese natürlich einfach verwenden. Das ist aber nicht garantiert.

EDIT: Ok, es gibt tatsächlich einen total hackigen und etwas generischen Weg für Aggregate. (Ich habe nur die Konvertierung in Tupel geschrieben, diese haben einen Standardvergleichsoperator). Godbolt

n314159
quelle
Netter Hack! Leider bin ich mit C ++ 11 festgefahren, daher kann ich es nicht verwenden.
Fredrik Enetorp
2

C ++ 20 unterstützt Standardvergleiche

#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;
}
Selbie
quelle
1
Dies ist zwar eine sehr nützliche Funktion, beantwortet jedoch die gestellte Frage nicht. Das OP sagte "Ich kann die verwendeten Strukturen nicht ändern", was bedeutet, dass das OP sie selbst dann nicht verwenden kann, wenn C ++ 20-Standardgleichheitsoperatoren verfügbar sind, da die Standardoperatoren ==oder <=>nur ausgeführt werden können im Klassenumfang.
Nicol Bolas
Wie Nicol Bolas sagte, kann ich die Strukturen nicht ändern.
Fredrik Enetorp
1

Unter der Annahme von POD-Daten kopiert der Standardzuweisungsoperator nur Mitgliedsbytes. (Eigentlich nicht 100% sicher, nimm mein Wort nicht dafür)

Sie können dies zu Ihrem Vorteil nutzen:

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; 
}
Kostas
quelle
@walnut Du hast recht, das war eine schreckliche Antwort. Schrieb einen um.
Kostas
Garantiert der Standard, dass die Zuweisung die Auffüllbytes unberührt lässt? Es gibt auch immer noch Bedenken hinsichtlich mehrerer Objektdarstellungen für denselben Wert in Grundtypen.
Walnuss
@ Walnut Ich glaube es tut .
Kostas
1
Die Kommentare unter der obersten Antwort in diesem Link scheinen darauf hinzudeuten, dass dies nicht der Fall ist. Die Antwort selbst besagt nur, dass die Polsterung nicht kopiert werden muss, aber nicht, dass sie nicht kopiert werden muss . Ich weiß es aber auch nicht genau.
Walnuss
Ich habe es jetzt getestet und es funktioniert nicht. Die Zuweisung lässt die Füllbytes nicht unberührt.
Fredrik Enetorp
0

Ich glaube, Sie können möglicherweise eine Lösung auf Antony Polukhins wunderbar verschlagenem Voodoo in der magic_getBibliothek finden - für Strukturen, nicht für komplexe Klassen.

Mit dieser Bibliothek können wir die verschiedenen Felder einer Struktur mit ihrem entsprechenden Typ in rein allgemeinem Vorlagencode iterieren. Antony hat dies beispielsweise verwendet, um beliebige Strukturen vollständig generisch in einen Ausgabestream mit den richtigen Typen streamen zu können. Es liegt auf der Hand, dass ein Vergleich auch eine mögliche Anwendung dieses Ansatzes sein könnte.

... aber du brauchst C ++ 14. Zumindest ist es besser als das C ++ 17 und spätere Vorschläge in anderen Antworten :-P

einpoklum
quelle