C ++ 'daki genel yapıları nasıl karşılaştırabilirim?


13

Yapıları genel bir şekilde karşılaştırmak istiyorum ve böyle bir şey yaptım (asıl kaynağı paylaşamıyorum, bu yüzden gerekirse daha fazla ayrıntı isteyin):

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

İki yapı örneği aynı üyeye sahip olsa bile (yanlış tutulma hata ayıklayıcısı ile kontrol ettim) bazen yanlış döndürür. Bazı aramalardan sonra memcmpkullanılan yapının yastıklı olması nedeniyle başarısız olabileceğini keşfettim .

Dolguya kayıtsız kalan belleği karşılaştırmanın daha uygun bir yolu var mı? Kullanılan yapıları değiştiremiyorum (kullandığım bir API'nın parçasılar) ve kullanılan birçok farklı yapı bazı farklı üyelere sahip ve bu nedenle (bildiklerime göre) tek tek karşılaştırılamıyor.

Düzenleme: Maalesef C ++ 11 ile sıkışıp kaldım. Bundan daha önce bahsetmeliydim ...


bunun başarısız olduğu bir örnek gösterebilir misiniz? Dolgu tek tipteki tüm örnekler için aynı olmalıdır, değil mi?
idclev 463035818

1
@ idclev463035818 Dolgu belirtilmemiş, değerini varsayamazsınız ve okumaya çalışmak için UB olduğuna inanıyorum (son bölümde emin değilim).
François Andrieux

@ idclev463035818 Doldurma, bellekte aynı göreli yerlerde bulunur ancak farklı verilere sahip olabilir. Yapının normal kullanımlarında atılır, böylece derleyici onu sıfırlamak için uğraşmayabilir.
NO_NAME

2
@ idclev463035818 Dolgu aynı boyutta. Dolgu oluşturan bitlerin durumu herhangi bir şey olabilir. Ne zaman memcmpkarşılaştırmanızda olanlar dolgu bitleri içerir.
François Andrieux

1
Yksisarvinen ile hemfikirim ... yapıları değil sınıfları kullanın ve ==operatörü uygulayın. Kullanmak memcmpgüvenilmez ve er ya da geç "bunu diğerlerinden biraz farklı yapmak" zorunda olan bir sınıfla uğraşıyorsunuz. Bunu bir operatöre uygulamak çok temiz ve etkilidir. Gerçek davranış polimorfik olacaktır, ancak kaynak kodu temiz olacaktır ... ve açıktır.
Mike Robinson

Yanıtlar:


7

Hayır, memcmpbunu yapmak uygun değildir. Ve C ++ 'da yansıma bu noktada bunu yapmak için yetersizdir (bunu yapmak için yeterince güçlü yansımayı destekleyen deneysel derleyiciler olacaktır ve ihtiyacınız olan özelliklere sahip olabilir).

Yerleşik yansıma olmadan, sorununuzu çözmenin en kolay yolu bazı manuel yansımalar yapmaktır.

Bunu al:

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

bunlardan ikisini karşılaştırabilmemiz için minimum miktarda iş yapmak istiyoruz.

Eğer sahipsek:

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

veya

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

için , o zaman:

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

oldukça iyi bir iş çıkarıyor.

Bu işlemi biraz çalışma ile özyinelemeli olarak genişletebiliriz; bağları karşılaştırmak yerine, bir şablona sarılmış her öğeyi karşılaştırın ve öğenin zaten çalışmadığı ve dizileri işlemediği sürece şablonun operator==bu kuralı yinelemeli olarak uygular ( as_tiekarşılaştırmak için öğeyi ==sarar).

Bu, bir üye başına "yansıma" verisi yazma ile birlikte bir kütüphane (100 satır kod?) Gerektirir. Sahip olduğunuz yapı sayısı sınırlıysa, yapı başına kodu manuel olarak yazmak daha kolay olabilir.


Muhtemelen almanın yolları var

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

as_tiekorkunç makrolar kullanarak yapı oluşturmak . Ama as_tieyeterince basit. Olarak tekrarlama rahatsız edici olduğu; bu kullanışlı:

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

bu durumda ve diğerleri. İle RETURNSyazmak as_tie:

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

tekrarın kaldırılması.


İşte özyinelemede bir bıçak:

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 (dizi) (tamamen özyinelemeli, dizilerin dizilerini bile destekler):

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

Canlı örnek .

Burada bir std::arrayof kullanıyorum refl_tie. Bu derleme zamanında önceki refl_tie grubumdan çok daha hızlı.

Ayrıca

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

kullanarak std::crefburada yerine std::tiederleme zamanı yükü üzerinde kurtarabilecek olarak crefçok daha basit bir sınıftır tuple.

Son olarak,

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

bu da dizi üyelerinin işaretçilere bozulmasını ve işaretçi eşitliğine (muhtemelen dizilerden istemediğiniz) geri düşmesini önler.

Bu olmadan, bir diziyi yansıtılmamış bir yapıya geçirirseniz, bu işaretçi-yansıtılmamış yapıya refl_tiegeri döner ve bu da saçma çalışır ve geri döner.

Bununla derleme zamanı hatası elde edersiniz.


Kütüphane türleriyle özyineleme desteği zor. Bunları yapabilirsiniz std::tie:

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

ancak bu özyinelemeyi desteklemez.


Bu tür bir çözümü manuel yansımalarla takip etmek istiyorum. Sağladığınız kod C ++ 11 ile çalışmıyor gibi görünüyor. Bana bu konuda yardım etme şansın var mı?
Fredrik Enetorp

1
Bunun C ++ 11'de çalışmamasının nedeni, sondaki dönüş türünün olmamasıdır as_tie. C ++ 14'ten başlayarak bu otomatik olarak çıkarılır. auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));C ++ 11 ile kullanabilirsiniz . Ya da dönüş türünü açıkça belirtin.
Darhuuk

1
@FredrikEnetorp Düzeltildi, ayrıca yazmayı kolaylaştıran bir makro. Tamamen özyinelemeli olarak çalışmasını sağlamak için yapılan iş (böylece alt yapıların as_tiedesteğinin bulunduğu, otomatik olarak çalıştığı bir yapı yapısı ) ve destek dizisi üyeleri ayrıntılı değildir, ancak mümkündür.
Yakk - Adam Nevraumont

Teşekkür ederim. Korkunç makroları biraz farklı yaptım, ama işlevsel olarak eşdeğerim. Sadece bir sorun daha. Ayrı bir başlık dosyasında karşılaştırmayı genelleştirmek ve çeşitli gmock test dosyalarına dahil etmeye çalışıyorum. Bu hata iletisiyle sonuçlanır: `` as_tie (Test1 const &) '' ın çoklu tanımı Onları satır içine almaya çalışıyorum ama işe yarayamıyorum.
Fredrik Enetorp

1
@FredrikEnetorp inlineAnahtar kelime birden çok tanım hatasını ortadan kaldırmalıdır . Çok az tekrarlanabilir bir örnek
Yakk - Adam Nevraumont

7

Doldurmanın, rasgele türleri bu şekilde karşılaştırma yolunda olduğu konusunda haklısınız.

Alabileceğiniz önlemler var:

  • Eğer kontrol sizde ise Dataörneğin gcc vardır __attribute__((packed)). Performans üzerinde etkisi vardır, ancak denemeye değer olabilir. Gerçi, packeddolguya tamamen izin vermemeye izin verip vermediğini bilmiyorum . Gcc doc diyor ki:

Yapı veya birleşim türü tanımına iliştirilen bu öznitelik, yapının veya birleşimin her üyesinin gereken belleği en aza indirecek şekilde yerleştirildiğini belirtir. Bir enum tanımına bağlandığında, en küçük integral tipinin kullanılması gerektiğini gösterir.

T TriviallyCopyable ise ve aynı değere sahip T tipi iki nesne aynı nesne temsiline sahipse, üye sabit değerine eşit true değerini sağlar. Diğer herhangi bir tür için değer yanlıştır.

ve Ötesi:

Bu özellik, bir türün nesne temsilini bir bayt dizisi olarak sağlayarak doğru bir karma yapıp yapamayacağını belirlemeyi mümkün kılmak için tanıtıldı.

Not: Sadece dolguya değindim, ancak bellekte farklı temsilleri olan örnekler için eşit olan türlerin hiçbir şekilde nadir olmadığını unutmayın (örneğin std::string, std::vectorve diğerleri).


1
Bu cevabı beğendim. Bu tür özellik sayesinde, memcmpdolgu içermeyen yapılarda kullanmak ve operator==yalnızca gerektiğinde uygulamak için SFINAE kullanabilirsiniz .
Yksisarvinen

Tamam teşekkürler. Bununla güvenli bir şekilde bazı manuel yansımalar yapmam gerektiği sonucuna varabilirim.
Fredrik Enetorp

6

Kısacası: Genel bir şekilde mümkün değil.

Sorun memcmpşu ki, dolgu rastgele veriler içerebilir ve bu nedenle memcmpbaşarısız olabilir. Dolgunun nerede olduğunu bulmanın bir yolu olsaydı, bu bitleri sıfırlayabilir ve daha sonra, üyelerin önemsiz bir şekilde karşılaştırılabilir olması durumunda eşitliği kontrol edecek veri temsillerini karşılaştırabilirsiniz (bu durum std::stringiki dizeden farklı işaretçiler içerir, ancak sivri iki karakter dizisi eşittir). Ama yapıların dolgusuna ulaşmanın bir yolunu bilmiyorum. Derleyicinize yapıları paketlemesini söylemeyi deneyebilirsiniz, ancak bu erişimi daha yavaş hale getirir ve gerçekten çalışması garanti edilmez.

Bunu uygulamanın en temiz yolu tüm üyeleri karşılaştırmaktır. Tabii ki bu genel bir şekilde mümkün değildir (C ++ 23 veya sonraki sürümlerinde zaman yansımaları ve meta sınıfları derleyene kadar). C ++ 20'den itibaren, bir varsayılan oluşturabilir, operator<=>ancak bu sadece bir üye işlevi olarak mümkün olacağını düşünüyorum, bu yüzden yine bu gerçekten geçerli değildir. Eğer şanslıysanız ve karşılaştırmak istediğiniz tüm yapıların bir operator==tanımlaması varsa, elbette bunu kullanabilirsiniz. Ancak bu garanti edilmez.

EDIT: Tamam, aslında agregalar için tamamen hileli ve biraz genel bir yol var. (Ben sadece tuples dönüşüm yazdım, bunlar varsayılan karşılaştırma operatörü var). Godbolt


İyi hack! Ne yazık ki, C ++ 11 ile sıkışıp kaldım, bu yüzden kullanamıyorum.
Fredrik Enetorp

2

C ++ 20 varsayılan komaparisonları destekler

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

1
Bu çok kullanışlı bir özellik olsa da, sorulan soruya cevap vermiyor. OP, "Kullanılan yapıları değiştiremiyorum" demişti, yani C ++ 20 varsayılan eşitlik operatörleri mevcut olsa bile, OP varsayılan olarak ==veya <=>operatörler yapılabileceğinden bunları kullanamaz. sınıf kapsamında.
Nicol Bolas

Nicol Bolas'ın dediği gibi, yapıları değiştiremiyorum.
Fredrik Enetorp

1

POD verileri varsayıldığında, varsayılan atama operatörü yalnızca üye baytları kopyalar. (aslında bundan% 100 emin değilim, sözüme güvenmeyin)

Bunu kendi yararınıza kullanabilirsiniz:

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

@ walnut Haklısın, bu korkunç bir cevaptı. Birini yeniden yaz.
Kostas

Standart, atamanın dolgu baytlarına dokunulmamasını garanti ediyor mu? Temel türlerde aynı değer için birden fazla nesne temsili konusunda hala bir endişe bulunmaktadır.
ceviz

@walnut İnanıyorum öyle .
Kostas

1
Bu bağlantıda en üstteki cevabın altındaki yorumlar, bunun olmadığını gösterir. Cevap kendisi sadece dolgu söylüyor gerekmez kopyalanamaz, ancak o şekilde musn't . Yine de emin değilim.
ceviz

Şimdi test ettim ve işe yaramıyor. Atama, dolgu baytlarına dokunulmaz.
Fredrik Enetorp

0

İnanıyorum ki, Antony Polukhin'in magic_getkütüphanedeki harika şeytani voodoo'suna bir çözüm katabilirsiniz - karmaşık sınıflar için değil, yapılar için.

Bu kütüphane ile, bir yapının farklı alanlarını, uygun türleriyle, yalnızca genel şablonda yineleyebiliyoruz. Antony bunu, örneğin, tamamen genel olarak, doğru türlere sahip bir çıkış akışına keyfi yapıları aktarabilmek için kullandı. Karşılaştırmanın aynı zamanda bu yaklaşımın olası bir uygulaması olabileceği de mantıklıdır.

... ama C ++ 14'e ihtiyacınız var. En azından diğer cevaplardaki C ++ 17 ve daha sonraki önerilerden daha iyidir:

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.