Comment comparer des structures génériques en C ++?

13

Je veux comparer les structures de manière générique et j'ai fait quelque chose comme ça (je ne peux pas partager la source réelle, alors demandez plus de détails si nécessaire):

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

Cela fonctionne principalement comme prévu, sauf qu'il retourne parfois false même si les deux instances de structure ont des membres identiques (j'ai vérifié avec le débogueur eclipse). Après quelques recherches, j'ai découvert que cela memcmppouvait échouer en raison du remplissage de la structure utilisée.

Existe-t-il un moyen plus approprié de comparer la mémoire indifférente au rembourrage? Je ne suis pas en mesure de modifier les structures utilisées (elles font partie d'une API que j'utilise) et les nombreuses structures différentes utilisées ont des membres différents et ne peuvent donc pas être comparées individuellement de manière générique (à ma connaissance).

Edit: je suis malheureusement bloqué avec C ++ 11. J'aurais dû le mentionner plus tôt ...

Fredrik Enetorp
la source
pouvez-vous montrer un exemple où cela échoue? Le remplissage doit être le même pour toutes les instances d'un même type, non?
idclev 463035818
1
@ idclev463035818 Le rembourrage n'est pas spécifié, vous ne pouvez pas assumer sa valeur et je crois que c'est UB pour essayer de le lire (pas sûr sur cette dernière partie).
François Andrieux
@ idclev463035818 Le remplissage est aux mêmes emplacements relatifs en mémoire mais il peut avoir des données différentes. Il est ignoré dans les utilisations normales de la structure afin que le compilateur ne prenne pas la peine de le mettre à zéro.
NO_NAME
2
@ idclev463035818 Le rembourrage a la même taille. L'état des bits qui constituent ce remplissage peut être n'importe quoi. Lorsque vous memcmpincluez ces bits de remplissage dans votre comparaison.
François Andrieux
1
Je suis d'accord avec Yksisarvinen ... utilisez des classes, pas des structures, et implémentez l' ==opérateur. L'utilisation memcmpn'est pas fiable et, tôt ou tard, vous aurez affaire à une classe qui doit «le faire un peu différemment des autres». C'est très propre et efficace de mettre cela en œuvre chez un opérateur. Le comportement réel sera polymorphe mais le code source sera propre ... et, évident.
Mike Robinson

Réponses:

7

Non, memcmpne convient pas pour cela. Et la réflexion en C ++ est insuffisante pour ce faire à ce stade (il y aura des compilateurs expérimentaux qui prennent en charge la réflexion suffisamment forte pour le faire déjà, et pourrait avoir les fonctionnalités dont vous avez besoin).

Sans réflexion intégrée, la façon la plus simple de résoudre votre problème consiste à effectuer une réflexion manuelle.

Prends ça:

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

nous voulons faire le minimum de travail afin de pouvoir comparer deux d'entre eux.

Si nous avons:

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

ou

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

pour , alors:

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

fait un travail assez décent.

Nous pouvons étendre ce processus pour qu'il soit récursif avec un peu de travail; au lieu de comparer les liens, comparez chaque élément encapsulé dans un modèle, et ce modèle operator==applique récursivement cette règle (encapsulant l'élément as_tiepour comparer) à moins que l'élément n'ait déjà un travail ==et gère les tableaux.

Cela nécessitera un peu de bibliothèque (100 lignes de code?) Avec l'écriture d'un peu de données de «réflexion» manuelles par membre. Si le nombre de structures que vous avez est limité, il pourrait être plus facile d'écrire manuellement du code par structure.


Il existe probablement des moyens

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

pour générer la as_tiestructure en utilisant d'horribles macros. Mais as_tiec'est assez simple. En la répétition est ennuyeuse; c'est utile:

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

dans cette situation et bien d'autres. Avec RETURNS, écrire as_tiec'est:

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

supprimer la répétition.


Voici un coup de couteau pour le rendre récursif:

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) (entièrement récursif, prend même en charge les tableaux de tableaux):

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

Exemple en direct .

Ici, j'utilise un std::arrayde refl_tie. C'est beaucoup plus rapide que mon précédent tuple de refl_tie au moment de la compilation.

Aussi

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

utiliser std::crefici au lieu de std::tiepourrait économiser sur la surcharge au moment de la compilation, comme crefc'est une classe beaucoup plus simple que tuple.

Enfin, vous devez ajouter

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

ce qui empêchera les membres du tableau de se décomposer en pointeurs et de retomber sur l'égalité du pointeur (ce que vous ne voulez probablement pas des tableaux).

Sans cela, si vous passez un tableau à une structure non réfléchie, il retombe sur une structure pointeur vers non réfléchie refl_tie, qui fonctionne et renvoie un non-sens.

Avec cela, vous vous retrouvez avec une erreur de compilation.


La prise en charge de la récursivité à travers les types de bibliothèques est délicate. Vous pourriez std::tieles:

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

mais cela ne prend pas en charge la récursivité à travers elle.

Yakk - Adam Nevraumont
la source
J'aimerais poursuivre ce type de solution avec des réflexions manuelles. Le code que vous avez fourni ne semble pas fonctionner avec C ++ 11. Est-ce que tu peux m'aider avec ça?
Fredrik Enetorp
1
La raison pour laquelle cela ne fonctionne pas en C ++ 11 est le manque de type de retour de fin activé as_tie. À partir de C ++ 14, cela se déduit automatiquement. Vous pouvez utiliser auto as_tie (some_struct const & s) -> decltype(std::tie(s.x, s.d1, s.d2, s.c));en C ++ 11. Ou indiquez explicitement le type de retour.
Darhuuk
1
@FredrikEnetorp Fixed, plus une macro qui facilite l'écriture. Le travail pour le faire fonctionner de manière récursive (donc un struct-of-struct, où les sous-structures ont un as_tiesupport, fonctionne automatiquement) et les membres du tableau de support ne sont pas détaillés, mais c'est possible.
Yakk - Adam Nevraumont
Je vous remercie. J'ai fait les horribles macros un peu différemment, mais fonctionnellement équivalentes. Encore un problème. J'essaie de généraliser la comparaison dans un fichier d'en-tête séparé et de l'inclure dans divers fichiers de test gmock. Cela entraîne le message d'erreur: définition multiple de `as_tie (Test1 const &) 'J'essaie de les aligner mais je ne peux pas le faire fonctionner.
Fredrik Enetorp
1
@FredrikEnetorp Le inlinemot-clé devrait faire disparaître les erreurs de définition multiples. Utilisez le bouton [poser une question] après avoir obtenu un exemple reproductible minimal
Yakk - Adam Nevraumont
7

Vous avez raison de dire que le remplissage vous empêche de comparer des types arbitraires de cette manière.

Vous pouvez prendre des mesures:

  • Si vous contrôlez Dataalors par exemple gcc a __attribute__((packed)). Cela a un impact sur les performances, mais cela pourrait valoir la peine de l'essayer. Cependant, je dois admettre que je ne sais pas si packedvous permet d'interdire complètement le rembourrage. Le doc Gcc dit:

Cet attribut, attaché à la définition de type struct ou union, spécifie que chaque membre de la structure ou union est placé pour minimiser la mémoire requise. Lorsqu'il est attaché à une définition d'énumération, il indique que le plus petit type intégral doit être utilisé.

Si T est TriviallyCopyable et si deux objets de type T avec la même valeur ont la même représentation d'objet, la valeur de la constante de membre est égale à true. Pour tout autre type, la valeur est false.

et plus loin:

Cette caractéristique a été introduite pour permettre de déterminer si un type peut être correctement haché en hachant sa représentation d'objet comme un tableau d'octets.

PS: je n'ai abordé que le remplissage, mais n'oubliez pas que les types qui peuvent comparer des instances égales avec une représentation différente en mémoire ne sont en aucun cas rares (par exemple std::string, std::vectoret bien d'autres).

idclev 463035818
la source
1
J'aime cette réponse. Avec ce trait de type, vous pouvez utiliser SFINAE pour l'utiliser memcmpsur des structures sans remplissage et operator==ne l' implémenter qu'en cas de besoin.
Yksisarvinen
OK merci. Avec cela, je peux conclure en toute sécurité que je dois faire quelques réflexions manuelles.
Fredrik Enetorp
6

En bref: pas possible de manière générique.

Le problème avec memcmpest que le remplissage peut contenir des données arbitraires et donc memcmppeut échouer. S'il y avait un moyen de savoir où se trouve le remplissage, vous pourriez mettre à zéro ces bits puis comparer les représentations de données, cela vérifierait l'égalité si les membres sont trivialement comparables (ce qui n'est pas le cas, std::stringcar deux chaînes peuvent contiennent des pointeurs différents, mais les deux tableaux de caractères pointés sont égaux). Mais je ne connais aucun moyen d'accéder au remplissage des structures. Vous pouvez essayer de dire à votre compilateur de compresser les structures, mais cela ralentira les accès et ne sera pas vraiment garanti de fonctionner.

La façon la plus simple de mettre en œuvre cela est de comparer tous les membres. Bien sûr, cela n'est pas vraiment possible de manière générique (jusqu'à ce que nous obtenions des réflexions de temps de compilation et des méta-classes en C ++ 23 ou version ultérieure). A partir de C ++ 20, on pourrait générer une valeur par défaut operator<=>mais je pense que cela ne serait également possible qu'en tant que fonction membre donc, encore une fois, cela n'est pas vraiment applicable. Si vous êtes chanceux et que toutes les structures que vous souhaitez comparer ont un operator==défini, vous pouvez bien sûr simplement l'utiliser. Mais ce n'est pas garanti.

EDIT: Ok, il existe en fait un moyen totalement hacky et quelque peu générique pour les agrégats. (J'ai seulement écrit la conversion en tuples, ceux-ci ont un opérateur de comparaison par défaut). Godbolt

n314159
la source
Joli hack! Malheureusement, je suis bloqué avec C ++ 11, donc je ne peux pas l'utiliser.
Fredrik Enetorp
2

C ++ 20 prend en charge les comparaisons par défaut

#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
la source
1
Bien que cette fonctionnalité soit très utile, elle ne répond pas à la question posée. L'OP a dit "Je ne suis pas en mesure de modifier les structures utilisées", ce qui signifie que, même si les opérateurs d'égalité par défaut C ++ 20 étaient disponibles, l'OP ne pourrait pas les utiliser car la valeur par défaut des opérateurs ==ou <=>ne peut être effectuée que au niveau de la classe.
Nicol Bolas
Comme l'a dit Nicol Bolas, je ne peux pas modifier les structures.
Fredrik Enetorp
1

En supposant des données POD, l'opérateur d'affectation par défaut copie uniquement les octets membres. (en fait, je n'en suis pas sûr à 100%, ne me croyez pas sur parole)

Vous pouvez utiliser ça à votre avantage:

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
la source
@walnut Vous avez raison, c'était une réponse terrible. Réécrit un.
Kostas
La norme garantit-elle que l'affectation laisse les octets de remplissage intacts? Il existe également une préoccupation concernant les représentations d'objets multiples pour la même valeur dans les types fondamentaux.
noyer
@walnut Je crois qu'il fait .
Kostas
1
Les commentaires sous la première réponse de ce lien semblent indiquer que ce n'est pas le cas. La réponse elle-même dit seulement que le rembourrage n'a pas besoin d' être copié, mais pas qu'il ne le doit pas . Mais je n'en suis pas sûr non plus.
noyer
Je l'ai maintenant testé et cela ne fonctionne pas. L'affectation ne laisse pas les octets de remplissage intacts.
Fredrik Enetorp
0

Je pense que vous pourrez peut-être baser une solution sur le vaudou merveilleusement sournois d'Antony Polukhin dans la magic_getbibliothèque - pour les structures, pas pour les classes complexes.

Avec cette bibliothèque, nous sommes en mesure d'itérer les différents champs d'une structure, avec leur type approprié, en code purement général. Antony l'a utilisé, par exemple, pour pouvoir diffuser des structures arbitraires dans un flux de sortie avec les types corrects, de manière complètement générique. Il va de soi que la comparaison pourrait également être une application possible de cette approche.

... mais vous auriez besoin de C ++ 14. Au moins, c'est mieux que le C ++ 17 et les suggestions ultérieures dans d'autres réponses :-P

einpoklum
la source