Bagaimana cara membandingkan struct generik dalam C ++?

13

Saya ingin membandingkan struct dengan cara yang umum dan saya telah melakukan sesuatu seperti ini (saya tidak dapat membagikan sumber yang sebenarnya, jadi tanyakan lebih detail jika perlu):

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

Ini sebagian besar berfungsi sebagaimana mestinya, kecuali kadang-kadang mengembalikan false meskipun dua contoh inst memiliki anggota yang identik (saya sudah memeriksa dengan eclipse debugger). Setelah beberapa pencarian saya menemukan bahwa memcmpbisa gagal karena struct yang digunakan sedang diisi.

Apakah ada cara yang lebih tepat untuk membandingkan memori yang acuh tak acuh dengan bantalan? Saya tidak dapat memodifikasi struct yang digunakan (mereka adalah bagian dari API yang saya gunakan) dan banyak struct yang berbeda digunakan memiliki beberapa anggota yang berbeda dan dengan demikian tidak dapat dibandingkan secara individual dengan cara yang umum (setahu saya).

Sunting: Saya sayangnya terjebak dengan C ++ 11. Seharusnya menyebutkan ini sebelumnya ...

Fredrik Enetorp
sumber
dapatkah Anda menunjukkan contoh di mana ini gagal? Padding harus sama untuk semua instance dari satu jenis, bukan?
idclev 463035818
1
@ idclev463035818 Padding tidak ditentukan, Anda tidak dapat menganggap nilainya dan saya yakin itu UB untuk mencoba membacanya (tidak yakin pada bagian terakhir itu).
François Andrieux
@ idclev463035818 Padding berada di tempat relatif yang sama dalam memori tetapi dapat memiliki data yang berbeda. Itu dibuang dalam penggunaan normal dari struct sehingga kompiler mungkin tidak repot-repot untuk nol itu.
NO_NAME
2
@ idclev463035818 Padding memiliki ukuran yang sama. Keadaan bit yang membentuk padding itu bisa apa saja. Ketika Anda memcmpAnda memasukkan bit padding dalam perbandingan Anda.
François Andrieux
1
Saya setuju dengan Yksisarvinen ... menggunakan kelas, bukan struct, dan mengimplementasikan ==operator. Menggunakan memcmptidak dapat diandalkan, dan cepat atau lambat Anda akan berhadapan dengan beberapa kelas yang harus "melakukannya sedikit berbeda dari yang lain." Sangat bersih dan efisien untuk mengimplementasikannya pada operator. Perilaku aktual akan menjadi polimorfik tetapi kode sumbernya akan bersih ... dan, jelas.
Mike Robinson

Jawaban:

7

Tidak, memcmptidak cocok untuk melakukan ini. Dan refleksi dalam C ++ tidak cukup untuk melakukan ini pada saat ini (akan ada kompiler eksperimental yang mendukung refleksi yang cukup kuat untuk melakukan ini, dan mungkin memiliki fitur yang Anda butuhkan).

Tanpa refleksi bawaan, cara termudah untuk menyelesaikan masalah Anda adalah dengan melakukan refleksi manual.

Ambil ini:

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

kami ingin melakukan jumlah pekerjaan minimal sehingga kami dapat membandingkan dua di antaranya.

Jika kita memiliki:

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

atau

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

untuk , maka:

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

melakukan pekerjaan yang cukup baik.

Kita dapat memperluas proses ini menjadi rekursif dengan sedikit pekerjaan; alih-alih membandingkan ikatan, bandingkan setiap elemen yang dibungkus dengan templat, dan templat itu operator==secara rekursif menerapkan aturan ini (membungkus elemen as_tieuntuk membandingkan) kecuali elemen tersebut sudah memiliki kerja== , dan menangani array.

Ini akan membutuhkan sedikit perpustakaan (100ish baris kode?) Bersama dengan menulis sedikit data "refleksi" manual per anggota. Jika jumlah struct yang Anda miliki terbatas, mungkin lebih mudah untuk menulis kode per-struct secara manual.


Mungkin ada cara untuk mendapatkannya

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

untuk menghasilkan as_tiestruktur menggunakan macro yang mengerikan. Tetapi as_tiecukup sederhana. Dalam pengulangan itu menjengkelkan; ini berguna:

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

dalam situasi ini dan banyak lainnya. Dengan RETURNS, menulis as_tieadalah:

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

menghapus pengulangan.


Berikut ini adalah cara membuatnya rekursif:

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) (sepenuhnya rekursif, bahkan mendukung array-of-array):

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

Contoh langsung .

Di sini saya menggunakan std::arraydari refl_tie. Ini jauh lebih cepat daripada tuple refl_tie saya sebelumnya pada waktu kompilasi.

Juga

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

menggunakan di std::crefsini bukannya std::tiebisa menghemat waktu kompilasi, seperti crefkelas yang jauh lebih sederhana daripada tuple.

Akhirnya, Anda harus menambahkan

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

yang akan mencegah anggota array dari peluruhan ke pointer dan jatuh kembali pada pointer-kesetaraan (yang Anda mungkin tidak inginkan dari array).

Tanpa ini, jika Anda meneruskan array ke struct yang tidak direfleksikan, ia jatuh kembali ke pointer ke struct yang tidak direfleksikan refl_tie , yang berfungsi dan mengembalikan omong kosong.

Dengan ini, Anda berakhir dengan kesalahan waktu kompilasi.


Dukungan untuk rekursi melalui tipe perpustakaan cukup rumit. Anda dapat std::tiemelakukannya:

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

tetapi itu tidak mendukung rekursi melalui itu.

Yakk - Adam Nevraumont
sumber
Saya ingin mengejar solusi jenis ini dengan refleksi manual. Kode yang Anda berikan tampaknya tidak berfungsi dengan C ++ 11. Apakah ada kesempatan yang bisa Anda bantu?
Fredrik Enetorp
1
Alasan ini tidak berfungsi di C ++ 11 adalah kurangnya tipe trailing return as_tie. Mulai dari C ++ 14 ini disimpulkan secara otomatis. Anda dapat menggunakan auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));di C ++ 11. Atau secara eksplisit nyatakan tipe pengembalian.
Darhuuk
1
@FredrikEnetorp Tetap, ditambah makro yang membuatnya mudah untuk ditulis. Pekerjaan untuk membuatnya bekerja secara penuh secara rekursif (jadi struct-of-struct, di mana substruktur memiliki as_tiedukungan, secara otomatis bekerja) dan anggota array dukungan tidak dirinci, tetapi mungkin.
Yakk - Adam Nevraumont
Terima kasih. Saya melakukan macro mengerikan sedikit berbeda, tetapi secara fungsional setara. Hanya satu masalah lagi. Saya mencoba untuk menggeneralisasi perbandingan dalam file header terpisah dan memasukkannya dalam berbagai file tes gmock. Ini menghasilkan pesan kesalahan: definisi ganda dari `as_tie (Test1 const &) 'Saya mencoba untuk menyejajarkannya tetapi tidak dapat membuatnya berfungsi.
Fredrik Enetorp
1
@FredrikEnetorp Kata inlinekunci seharusnya membuat kesalahan definisi banyak hilang. Gunakan tombol [tanyakan] setelah Anda mendapatkan contoh minimal yang dapat direproduksi
Yakk - Adam Nevraumont
7

Anda benar bahwa padding menghalangi cara Anda membandingkan tipe arbitrer dengan cara ini.

Ada beberapa langkah yang dapat Anda ambil:

  • Jika Anda mengendalikan Datamaka misalnya gcc __attribute__((packed)). Ini berdampak pada kinerja, tetapi mungkin patut untuk dicoba. Meskipun, saya harus mengakui bahwa saya tidak tahu apakah packedmemungkinkan Anda untuk melarang padding sepenuhnya. Gcc doc mengatakan:

Atribut ini, terlampir pada definisi tipe struct atau union, menetapkan bahwa setiap anggota struktur atau union ditempatkan untuk meminimalkan memori yang diperlukan. Ketika dilampirkan ke definisi enum, itu menunjukkan bahwa tipe integral terkecil harus digunakan.

Jika T adalah TriviallyCopyable dan jika ada dua objek tipe T dengan nilai yang sama memiliki representasi objek yang sama, memberikan nilai konstanta anggota sama dengan true. Untuk jenis lainnya, nilainya salah.

dan selanjutnya:

Sifat ini diperkenalkan untuk memungkinkan untuk menentukan apakah suatu tipe dapat di hash dengan benar dengan hashing representasi objeknya sebagai array byte.

PS: Saya hanya membahas padding, tapi jangan lupa bahwa tipe yang dapat membandingkan sama untuk contoh dengan representasi berbeda dalam memori sama sekali tidak jarang (misalnya std::string, std::vectordan banyak lainnya).

idclev 463035818
sumber
1
Saya suka jawaban ini. Dengan sifat jenis ini, Anda dapat menggunakan SFINAE untuk digunakan memcmppada struct tanpa bantalan dan operator==hanya menerapkan bila diperlukan.
Yksisarvinen
Ok terima kasih. Dengan ini saya dapat menyimpulkan bahwa saya perlu melakukan beberapa refleksi manual.
Fredrik Enetorp
6

Singkatnya: Tidak mungkin secara umum.

Masalahnya memcmpadalah padding mungkin berisi data yang berubah-ubah dan karenanya memcmpmungkin gagal. Jika ada cara untuk mencari tahu di mana padding berada, Anda bisa membidik bit-bit itu dan kemudian membandingkan representasi data, yang akan memeriksa kesetaraan jika anggota sebanding secara sepele (yang bukan kasus yaitu untuk std::stringkarena dua string dapat berisi pointer yang berbeda, tetapi dua array char-runcing sama). Tapi saya tahu tidak ada cara untuk mendapatkan pad dari struct. Anda dapat mencoba untuk memberitahu kompiler Anda untuk mengemas struct, tetapi ini akan membuat akses lebih lambat dan tidak benar-benar dijamin untuk bekerja.

Cara terbersih untuk mengimplementasikan ini adalah membandingkan semua anggota. Tentu saja ini tidak benar-benar mungkin dengan cara yang umum (sampai kita mendapatkan refleksi waktu kompilasi dan kelas meta di C ++ 23 atau lebih baru). Dari C ++ 20 dan seterusnya, seseorang dapat menghasilkan default operator<=>tapi saya pikir ini juga hanya mungkin sebagai fungsi anggota jadi, sekali lagi ini tidak benar-benar berlaku. Jika Anda beruntung dan semua struct yang ingin Anda bandingkan telah operator==ditentukan, Anda tentu saja dapat menggunakannya. Tapi itu tidak dijamin.

EDIT: Ok, sebenarnya ada cara yang benar-benar rumit dan agak umum untuk agregat. (Saya hanya menulis konversi ke tuple, yang memiliki operator perbandingan default). godbolt

n314159
sumber
Hack bagus! Sayangnya, saya terjebak dengan C ++ 11 jadi saya tidak bisa menggunakannya.
Fredrik Enetorp
2

C ++ 20 mendukung comaparisons default

#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
sumber
1
Meskipun itu adalah fitur yang sangat berguna, itu tidak menjawab pertanyaan seperti yang ditanyakan. OP memang mengatakan "Saya tidak dapat memodifikasi struct yang digunakan", yang berarti bahwa, bahkan jika operator standar kesetaraan C ++ 20 tersedia, OP tidak akan dapat menggunakannya karena pengaturan default ==atau <=>operator hanya dapat dilakukan di lingkup kelas.
Nicol Bolas
Seperti kata Nicol Bolas, saya tidak bisa memodifikasi struct.
Fredrik Enetorp
1

Dengan asumsi data POD, operator penugasan standar hanya menyalin byte anggota. (sebenarnya tidak 100% yakin tentang itu, jangan mengambil kata saya untuk itu)

Anda dapat menggunakan ini untuk keuntungan Anda:

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
sumber
@walnut Anda benar, itu jawaban yang mengerikan. Menulis ulang satu.
Kostas
Apakah standar menjamin bahwa tugas meninggalkan padding byte tidak tersentuh? Masih ada kekhawatiran tentang representasi objek berganda untuk nilai yang sama dalam tipe fundamental.
walnut
@walnut, saya percaya begitu .
Kostas
1
Komentar di bawah jawaban teratas di tautan itu tampaknya mengindikasikan tidak. Jawaban itu sendiri hanya mengatakan bahwa padding tidak perlu disalin, tetapi tidak bahwa itu musn't . Aku juga tidak tahu pasti.
walnut
Sekarang saya sudah mengujinya dan tidak berhasil. Tugas tidak membiarkan byte padding tersentuh.
Fredrik Enetorp
0

Saya percaya Anda mungkin dapat mendasarkan solusi pada voodoo Antony Polukhin yang luar biasa licik di magic_getperpustakaan - untuk struct, bukan untuk kelas yang kompleks.

Dengan pustaka itu, kita dapat mengulangi bidang-bidang yang berbeda dari sebuah struct, dengan tipe yang sesuai, dalam kode yang murni umum. Antony telah menggunakan ini, misalnya, untuk dapat melakukan streaming struct sewenang-wenang ke aliran output dengan jenis yang benar, sepenuhnya umum. Masuk akal bahwa perbandingan mungkin juga merupakan aplikasi yang mungkin dari pendekatan ini.

... tetapi Anda membutuhkan C ++ 14. Setidaknya itu lebih baik daripada C ++ 17 dan saran selanjutnya dalam jawaban lain :-P

einpoklum
sumber