Jak porównać ogólne struktury w C ++?

13

Chcę porównać struktury w ogólny sposób i zrobiłem coś takiego (nie mogę udostępnić faktycznego źródła, więc w razie potrzeby poproś o więcej szczegółów):

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

Działa to głównie zgodnie z przeznaczeniem, z tym że czasami zwraca false, mimo że dwie instancje struct mają identyczne elementy (sprawdziłem za pomocą debugera Eclipse). Po kilku poszukiwaniach odkryłem, że memcmpmoże się nie udać z powodu wypełnienia użytej struktury.

Czy istnieje bardziej odpowiedni sposób porównywania pamięci, która jest obojętna na wypełnianie? Nie jestem w stanie modyfikować używanych struktur (są one częścią interfejsu API, którego używam), a wiele różnych używanych struktur ma różne elementy i dlatego nie można ich porównywać indywidualnie w sposób ogólny (o ile mi wiadomo).

Edycja: Niestety niestety utknąłem w C ++ 11. Powinienem był o tym wspomnieć wcześniej ...

Fredrik Enetorp
źródło
czy możesz podać przykład, w którym to się nie udaje? Wypełnienie powinno być takie samo dla wszystkich instancji jednego typu, nie?
idclev 463035818
1
@ idclev463035818 Padding jest nieokreślony, nie możesz założyć, że jest on wartościowy i uważam, że to UB próbuje go przeczytać (nie jestem pewien w tej ostatniej części).
François Andrieux
@ idclev463035818 Wypełnienie znajduje się w tych samych względnych miejscach w pamięci, ale może mieć różne dane. Jest on odrzucany w normalnych zastosowaniach struktury, więc kompilator może nie zadać sobie trudu, aby go wyzerować.
NO_NAME
2
@ idclev463035818 Wypełnienie ma ten sam rozmiar. Stan bitów składających się na wypełnienie może być dowolny. Po memcmpuwzględnieniu tych bitów wypełniających w porównaniu.
François Andrieux
1
Zgadzam się z Yksisarvinenem ... używaj klas, a nie struktur i implementuj ==operator. Używanie memcmpjest niewiarygodne, a wcześniej czy później będziesz miał do czynienia z pewną klasą, która musi „zrobić to trochę inaczej niż inne”. Zaimplementowanie tego u operatora jest bardzo czyste i wydajne. Rzeczywiste zachowanie będzie polimorficzne, ale kod źródłowy będzie czysty ... i oczywiście.
Mike Robinson

Odpowiedzi:

7

Nie, memcmpnie nadaje się do tego. Odbicie w C ++ jest w tym momencie niewystarczające (będą kompilatory eksperymentalne, które obsługują odbicie wystarczająco silne, aby to zrobić już, i może mieć potrzebne funkcje).

Bez wbudowanego odbicia najłatwiejszym sposobem rozwiązania problemu jest ręczne odbicie.

Weź to:

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

chcemy wykonać minimalną ilość pracy, abyśmy mogli porównać dwa z nich.

Jeśli mamy:

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

lub

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

dla , a następnie:

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

wykonuje całkiem przyzwoitą robotę.

Możemy rozszerzyć ten proces na rekurencyjny przy odrobinie pracy; zamiast porównywać więzi, porównaj każdy element zawinięty w szablon, a szablon operator==rekurencyjnie stosuje tę regułę (zawijanie elementu w as_tiecelu porównania), chyba że element ma już działający== , i nie obsługuje tablic.

Będzie to wymagało odrobiny biblioteki (100 linii wiersza kodu?) Wraz z zapisaniem odrobiny ręcznych danych „refleksyjnych” dla poszczególnych członków. Jeśli liczba posiadanych struktur jest ograniczona, łatwiejsze może być ręczne napisanie kodu dla struktury.


Prawdopodobnie istnieją sposoby na zdobycie

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

do generowania as_tiestruktury przy użyciu okropnych makr. Ale as_tieto jest dość proste. W powtórzenie jest denerwujące; jest to przydatne:

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

w tej sytuacji i wielu innych. Dzięki RETURNS, pisanie as_tiejest:

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

usunięcie powtórzenia.


Oto próba uczynienia go rekurencyjnym:

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 (tablica) (w pełni rekurencyjny, obsługuje nawet tablice tablic):

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

Przykład na żywo .

Tutaj używam std::arrayod refl_tie. Jest to znacznie szybsze niż moja poprzednia krotka refl_tie w czasie kompilacji.

Również

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

użycie std::creftutaj zamiast std::tiemoże zaoszczędzić na nakładach czasu kompilacji, ponieważ crefjest to znacznie prostsza klasa niż tuple.

Na koniec powinieneś dodać

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

co zapobiegnie rozkładaniu się elementów tablicy na wskaźniki i cofaniu się do równości wskaźnika (czego prawdopodobnie nie chcesz od tablic).

Bez tego, jeśli przekażesz tablicę do struktury nie odzwierciedlonej, wraca ona do struktury wskaźnikowej do nie odzwierciedlonej refl_tie , która działa i zwraca bzdury.

Z tego wynika błąd czasu kompilacji.


Obsługa rekurencji za pomocą typów bibliotek jest trudna. Mógłbyś std::tieim:

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

ale to nie obsługuje rekurencji.

Jak - Adam Nevraumont
źródło
Chciałbym realizować tego typu rozwiązanie z ręcznymi odbiciami. Podany kod nie działa z C ++ 11. Czy jest szansa, że ​​możesz mi w tym pomóc?
Fredrik Enetorp
1
Powodem, dla którego to nie działa w C ++ 11 jest brak końcowego typu return as_tie. Począwszy od C ++ 14 jest to wydedukowane automatycznie. Możesz używać auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));w C ++ 11. Lub wyraźnie podaj typ zwrotu.
Darhuuk
1
@FredrikEnetorp Naprawiono, a także makro, które ułatwia pisanie. Praca nad tym, aby była w pełni rekurencyjna (tak więc struct-of-struct, w której substraty mają as_tiewsparcie, działa automatycznie) i członkowie tablicy wsparcia nie są szczegółowe, ale jest to możliwe.
Jak - Adam Nevraumont,
Dziękuję Ci. Zrobiłem straszne makra nieco inaczej, ale funkcjonalnie równoważne. Jeszcze tylko jeden problem. Próbuję uogólnić porównanie w osobnym pliku nagłówkowym i dołączyć go do różnych plików testowych gmock. Powoduje to wyświetlenie komunikatu o błędzie: wielokrotna definicja „as_tie (Test1 const &)” Próbuję je wstawić, ale nie mogę go uruchomić.
Fredrik Enetorp
1
@FredrikEnetorp Słowo inlinekluczowe powinno spowodować zniknięcie wielu błędów definicji. Użyj przycisku [zadaj pytanie] po otrzymaniu minimalnego odtwarzalnego przykładu
Jak - Adam Nevraumont
7

Masz rację, że wypełnienie przeszkadza ci w porównywaniu dowolnych typów w ten sposób.

Istnieją środki, które możesz podjąć:

  • Jeśli masz nad tym kontrolę, Datanp. Gcc ma __attribute__((packed)). Wpływa to na wydajność, ale warto spróbować. Chociaż muszę przyznać, że nie wiem, czy packedpozwala ci całkowicie zabronić paddingowania. Dokument Gcc mówi:

Ten atrybut, dołączony do definicji typu struktury lub unii, określa, że ​​każdy element struktury lub unii jest umieszczony w celu zminimalizowania wymaganej pamięci. Po dołączeniu do definicji wyliczenia wskazuje, że należy użyć najmniejszego typu całki.

Jeśli T jest TrivialCopyable i jeśli dowolne dwa obiekty typu T o tej samej wartości mają tę samą reprezentację obiektu, zapewnia stałą wartość elementu równą true. Dla każdego innego typu wartością jest false.

i dalej:

Ta cecha została wprowadzona, aby umożliwić ustalenie, czy typ może być poprawnie zaszyfrowany przez zaszyfrowanie jego reprezentacji obiektu jako tablicy bajtów.

PS: Ja tylko skierowana wyściółkę, ale nie zapomnij, że typy, które można porównać równe dla przypadków z różnych reprezentacji w pamięci nie są bynajmniej rzadkich (np std::string, std::vectori wiele innych).

idclev 463035818
źródło
1
Podoba mi się ta odpowiedź. Dzięki tej funkcji możesz używać SFINAE do stosowania memcmpna strukturach bez wypełniania i implementować operator==tylko w razie potrzeby.
Yksisarvinen
Ok dzięki. Dzięki temu mogę spokojnie stwierdzić, że muszę zrobić kilka ręcznych refleksji.
Fredrik Enetorp
6

Krótko mówiąc: niemożliwe w sposób ogólny.

Problem z memcmp polega na tym, że wypełnienie może zawierać dowolne dane, a zatem memcmpmoże się nie powieść. Gdyby istniał sposób, aby dowiedzieć się, gdzie jest wypełnienie, można wyzerować te bity, a następnie porównać reprezentacje danych, co sprawdziłoby równość, jeśli elementy są trywialnie porównywalne (co nie jest prawdą, std::stringponieważ dwa ciągi znaków mogą zawierają różne wskaźniki, ale dwa spiczaste tablice znaków są równe). Ale nie znam sposobu, by dostać się do wypełnienia struktur. Możesz spróbować powiedzieć swojemu kompilatorowi, aby spakował struktury, ale spowoduje to spowolnienie dostępu i nie będzie tak naprawdę działać.

Najczystszym sposobem na wdrożenie tego jest porównanie wszystkich członków. Oczywiście nie jest to tak naprawdę możliwe w ogólny sposób (dopóki nie otrzymamy odbić czasowych i meta klas w C ++ 23 lub nowszych). Począwszy od C ++ 20 można generować wartość domyślną, operator<=>ale myślę, że byłoby to również możliwe tylko jako funkcja składowa, więc znowu nie ma to zastosowania. Jeśli masz szczęście i wszystkie struktury, które chcesz porównać, mają operator==zdefiniowane, możesz oczywiście tego po prostu użyć. Ale to nie jest gwarantowane.

EDYCJA: Ok, istnieje naprawdę zhackowany i nieco ogólny sposób na agregacje. (Napisałem konwersję tylko do krotek, te mają domyślny operator porównania). godbolt

n314159
źródło
Niezły hack! Niestety utknąłem w C ++ 11, więc nie mogę go używać.
Fredrik Enetorp
2

C ++ 20 obsługuje domyślne kombinacje

#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
źródło
1
Jest to bardzo przydatna funkcja, ale nie odpowiada na zadane pytanie. OP powiedział: „Nie jestem w stanie zmodyfikować używanych struktur”, co oznacza, że ​​nawet gdyby domyślni operatorzy równości C ++ 20 byli dostępni, OP nie byłby w stanie ich użyć, ponieważ domyślnie operatory ==lub <=>można wykonać tylko w zakresie zajęć.
Nicol Bolas,
Jak powiedział Nicol Bolas, nie mogę modyfikować struktur.
Fredrik Enetorp
1

Zakładając dane POD, domyślny operator przypisania kopiuje tylko bajty członka. (właściwie nie jestem w 100% tego pewien, nie wierz mi na słowo)

Możesz to wykorzystać na swoją korzyść:

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
źródło
@walnut Masz rację, to była okropna odpowiedź. Przepisałem jeden.
Kostas
Czy standard gwarantuje, że przydział pozostawia nietknięte bajty wypełniania? Nadal istnieją obawy dotyczące reprezentacji wielu obiektów dla tej samej wartości w typach podstawowych.
orzech
@walnut Myślę, że tak .
Kostas
1
Komentarze pod pierwszą odpowiedzią w tym linku wydają się wskazywać, że tak nie jest. Sama odpowiedź mówi tylko, że wypełnienie nie musi być kopiowane, ale nie, że nie musi . Ale też nie jestem tego pewien.
orzech
Teraz go przetestowałem i nie działa. Przypisanie nie pozostawia nietkniętych bajtów wypełniania.
Fredrik Enetorp
0

Myślę, że możesz oprzeć rozwiązanie na cudownie przebiegłym voodoo Antony'ego Polukhina w magic_getbibliotece - dla struktur, a nie dla złożonych klas.

Dzięki tej bibliotece jesteśmy w stanie iterować różne pola struktury, z ich odpowiednim typem, w kodzie o czysto ogólnym szablonie. Antony wykorzystał to na przykład, aby móc przesyłać dowolne struktury do strumienia wyjściowego o prawidłowych typach, całkowicie ogólnie. Jest oczywiste, że porównanie może być również możliwym zastosowaniem tego podejścia.

... ale potrzebujesz C ++ 14. Przynajmniej jest lepszy niż C ++ 17 i późniejsze sugestie w innych odpowiedziach :-P

einpoklum
źródło