Come confrontare le strutture generiche in C ++?


13

Voglio confrontare le strutture in modo generico e ho fatto qualcosa del genere (non posso condividere la fonte reale, quindi chiedere maggiori dettagli se necessario):

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

Funziona principalmente come previsto, tranne che a volte restituisce false anche se le due istanze struct hanno membri identici (ho verificato con il debugger di eclipse). Dopo alcune ricerche ho scoperto che memcmppuò fallire a causa della struttura usata che viene imbottita.

Esiste un modo più corretto di confrontare la memoria indifferente al padding? Non sono in grado di modificare le strutture utilizzate (fanno parte di un'API che sto usando) e le diverse strutture utilizzate hanno membri diversi e quindi non possono essere confrontate individualmente in modo generico (per quanto ne sappia).

Modifica: purtroppo sono bloccato con C ++ 11. Avrei dovuto menzionarlo prima ...


puoi mostrare un esempio in cui ciò fallisce? L'imbottitura dovrebbe essere la stessa per tutte le istanze di un tipo, no?
idclev 463035818,

1
@ idclev463035818 L'imbottitura non è specificata, non puoi assumere il suo valore e credo che sia UB provare a leggerlo (non sono sicuro in quest'ultima parte).
François Andrieux,

@ idclev463035818 Il riempimento si trova nelle stesse posizioni relative in memoria ma può avere dati diversi. Viene scartato negli usi normali della struttura, quindi il compilatore potrebbe non preoccuparsi di azzerarlo.
NO_NAME il

2
@ idclev463035818 L'imbottitura ha le stesse dimensioni. Lo stato dei bit che costituiscono quell'imbottitura può essere qualsiasi cosa. Quando memcmpincludi quei bit di padding nel tuo confronto.
François Andrieux,

1
Sono d'accordo con Yksisarvinen ... usare le classi, non le strutture, e implementare l' ==operatore. L'uso memcmpè inaffidabile, e prima o poi avrai a che fare con una classe che deve "farlo in modo leggermente diverso dagli altri". È molto pulito ed efficiente implementarlo in un operatore. Il comportamento effettivo sarà polimorfico ma il codice sorgente sarà pulito ... e, ovvio.
Mike Robinson,

Risposte:


7

No, memcmpnon è adatto a farlo. E la riflessione in C ++ non è sufficiente per fare questo a questo punto (ci saranno compilatori sperimentali che supportano la riflessione abbastanza forte da farlo già, e potrebbe avere le caratteristiche di cui hai bisogno).

Senza la riflessione integrata, il modo più semplice per risolvere il problema è eseguire una riflessione manuale.

Prendi questo:

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

vogliamo fare la minima quantità di lavoro in modo da poter confrontare due di questi.

Se abbiamo:

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

o

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

per , quindi:

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

fa un lavoro abbastanza decente.

Possiamo estendere questo processo in modo ricorsivo con un po 'di lavoro; invece di confrontare i legami, confronta ogni elemento racchiuso in un modello e quel modello operator==applica ricorsivamente questa regola (avvolgendo l'elemento in as_tieconfronto) a meno che l'elemento non abbia già un funzionamento ==e gestisca le matrici.

Ciò richiederà un po 'di libreria (100 righe di codice?) Insieme alla scrittura di un po' di dati "riflessi" manuali per membro. Se il numero di strutture che hai è limitato, potrebbe essere più semplice scrivere manualmente il codice per struttura.


Probabilmente ci sono modi per ottenere

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

per generare la as_tiestruttura usando macro orribili. Ma as_tieè abbastanza semplice. In la ripetizione è fastidiosa; questo è utile:

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

in questa situazione e molti altri. Con RETURNS, la scrittura as_tieè:

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

rimuovendo la ripetizione.


Ecco una pugnalata per renderlo ricorsivo:

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) (completamente ricorsivo, supporta anche array di 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>{} ) )

Esempio dal vivo .

Qui uso un std::arraydi refl_tie. Questo è molto più veloce della mia precedente tupla di refl_tie al momento della compilazione.

Anche

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

usare std::crefqui invece di std::tiepotrebbe risparmiare sui tempi di compilazione, in quanto crefè una classe molto più semplice di tuple.

Infine, dovresti aggiungere

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

che impedirà ai membri dell'array di decadere in puntatori e ricadere sull'uguaglianza dei puntatori (che probabilmente non si desidera dagli array).

Senza questo, se si passa un array a una struttura non riflessa, si ricorre alla struttura puntatore a non riflessa refl_tie, che funziona e restituisce assurdità.

Con questo, si finisce con un errore in fase di compilazione.


Il supporto per la ricorsione attraverso i tipi di libreria è complicato. Potrestistd::tie loro:

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

ma ciò non supporta la ricorsione attraverso di essa.


Vorrei perseguire questo tipo di soluzione con riflessi manuali. Il codice che hai fornito non sembra funzionare con C ++ 11. Qualche possibilità che mi possa aiutare in questo?
Fredrik Enetorp,

1
Il motivo per cui questo non funziona in C ++ 11 è la mancanza del tipo di ritorno finale attivo as_tie. A partire da C ++ 14 questo viene dedotto automaticamente. È possibile utilizzare auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));in C ++ 11. O dichiarare esplicitamente il tipo restituito.
Darhuuk,

1
@FredrikEnetorp Risolto, oltre a una macro che semplifica la scrittura. Il lavoro per farlo funzionare in modo completamente ricorsivo (quindi una struttura strutturata, in cui i substrati hanno il as_tiesupporto, funziona automaticamente) e supportare i membri dell'array non è dettagliata, ma è possibile.
Yakk - Adam Nevraumont,

Grazie. Ho fatto le macro orribili in modo leggermente diverso, ma funzionalmente equivalente. Solo un altro problema. Sto cercando di generalizzare il confronto in un file di intestazione separato e includerlo in vari file di test di gmock. Ciò si traduce nel messaggio di errore: definizione multipla di `as_tie (Test1 const &) 'Sto cercando di incorporarli ma non riesco a farlo funzionare.
Fredrik Enetorp,

1
@FredrikEnetorp La inlineparola chiave dovrebbe far sparire più errori di definizione. Usa il pulsante [fai domanda] dopo aver ottenuto un esempio riproducibile minimo
Yakk - Adam Nevraumont

7

Hai ragione nel dire che il padding ti mette a confronto tipi arbitrari in questo modo.

Ci sono misure che puoi prendere:

  • Se hai il controllo, Dataad esempio, gcc ha __attribute__((packed)). Ha un impatto sulle prestazioni, ma potrebbe valere la pena provarlo. Tuttavia, devo ammettere che non so se packedti consente di non consentire il riempimento completo. Gcc doc dice:

Questo attributo, associato alla definizione del tipo di struttura o unione, specifica che ciascun membro della struttura o unione viene posizionato per ridurre al minimo la memoria richiesta. Se associato a una definizione enum, indica che deve essere utilizzato il tipo integrale più piccolo.

Se T è TriviallyCopyable e se due oggetti di tipo T con lo stesso valore hanno la stessa rappresentazione di oggetto, fornisce il valore della costante membro uguale true. Per qualsiasi altro tipo, il valore è falso.

e inoltre:

Questa caratteristica è stata introdotta per consentire di determinare se un tipo può essere correttamente eseguito l'hashing mediante hash della sua rappresentazione dell'oggetto come array di byte.

PS: ho affrontato solo il riempimento, ma non dimenticare che tipi che possono essere paragonati uguali per istanze con diversa rappresentazione in memoria non sono affatto rari (ad esempio std::string, std::vectore molti altri).


1
Mi piace questa risposta. Con questa caratteristica di tipo, puoi usare SFINAE per usare memcmpsu strutture senza imbottitura e implementare operator==solo quando necessario.
Yksisarvinen,

Ok grazie. Con ciò posso tranquillamente concludere che devo fare alcune riflessioni manuali.
Fredrik Enetorp,

6

In breve: non possibile in modo generico.

Il problema memcmpè che il riempimento può contenere dati arbitrari e quindi memcmppotrebbe non riuscire. Se ci fosse un modo per scoprire dove si trova il padding, potresti azzerare quei bit e quindi confrontare le rappresentazioni dei dati, che verificherebbe l'uguaglianza se i membri sono banalmente comparabili (il che non è il caso, ad esempio perstd::string perché due stringhe possono contengono diversi puntatori, ma i due char-array a punta sono uguali). Ma non conosco alcun modo per arrivare all'imbottitura delle strutture. Puoi provare a dire al tuo compilatore di impacchettare le strutture, ma questo renderà gli accessi più lenti e non è veramente garantito per funzionare.

Il modo più pulito per implementare questo è confrontare tutti i membri. Naturalmente questo non è realmente possibile in modo generico (fino a quando non avremo riflessioni sulla compilazione e meta-classi in C ++ 23 o successive). Da C ++ 20 in poi, si potrebbe generare un valore predefinito, operator<=>ma penso che questo sarebbe anche possibile solo come funzione membro, quindi, di nuovo, questo non è realmente applicabile. Se sei fortunato e tutte le strutture che vuoi confrontare hanno un operator==definito, puoi ovviamente usarlo. Ma questo non è garantito.

EDIT: Ok, in realtà esiste un modo totalmente confuso e in qualche modo generico per gli aggregati. (Ho scritto solo la conversione in tuple, quelle hanno un operatore di confronto predefinito). Godbolt


Bel trucco! Sfortunatamente, sono bloccato con C ++ 11, quindi non posso usarlo.
Fredrik Enetorp,

2

C ++ 20 supporta comaparison predefinite

#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
Sebbene sia una funzione molto utile, non risponde alla domanda come è stata posta. L'OP ha detto "Non sono in grado di modificare le strutture utilizzate", il che significa che, anche se fossero disponibili operatori di uguaglianza predefiniti C ++ 20, l'OP non sarebbe in grado di usarli poiché il default ==o gli <=>operatori possono essere eseguiti nell'ambito della classe.
Nicol Bolas,

Come ha detto Nicol Bolas, non posso modificare le strutture.
Fredrik Enetorp,

1

Supponendo che i dati POD, l'operatore di assegnazione predefinito copia solo i byte membri. (in realtà non ne sono sicuro al 100%, non crederci sulla parola)

Puoi usarlo a tuo vantaggio:

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 Hai ragione che è stata una risposta terribile. Riscriverne uno.
Kostas,

Lo standard garantisce che l'incarico lasci intatti i byte di riempimento? Esiste ancora una preoccupazione per le rappresentazioni di più oggetti per lo stesso valore nei tipi fondamentali.
noce,

@Walnut Credo di si .
Kostas,

1
I commenti sotto la risposta in alto in quel link sembrano indicare che non lo è. La risposta stessa dice solo che l'imbottitura non deve essere copiata, ma non che non debba . Neanche io lo so per certo.
noce,

Ora l'ho provato e non funziona. L'assegnazione non lascia intatti i byte di riempimento.
Fredrik Enetorp,

0

Credo che potresti essere in grado di basare una soluzione sul voodoo meravigliosamente subdolo di Antony Polukhin in magic_getbiblioteca - per le strutture, non per le classi complesse.

Con quella libreria, siamo in grado di iterare i diversi campi di una struttura, con il loro tipo appropriato, in un codice puramente generale. Antony lo ha usato, ad esempio, per essere in grado di eseguire lo streaming di strutture arbitrarie su un flusso di output con i tipi corretti, in modo completamente generico. È ovvio che il confronto potrebbe anche essere una possibile applicazione di questo approccio.

... ma avresti bisogno di C ++ 14. Almeno è meglio del C ++ 17 e dei suggerimenti successivi in ​​altre risposte :-P

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.