Comparaison des types polymorphes en C ++ 20

Oct 27 2020

J'ai du code qui se situe quelque part entre c ++ 17 et c ++ 20. Plus précisément, nous avons activé C ++ 20 sur GCC-9 et clang-9, où il n'est que partiellement implémenté.

Dans le code, nous avons une assez grande hiérarchie de types polymorphes comme ceci:

struct Identifier {
    virtual bool operator==(const Identifier&other) const = 0;
};

struct UserIdentifier : public Identifier {
    int userId =0;
    bool operator==(const Identifier&other) const override {
        const UserIdentifier *otherUser = dynamic_cast<const UserIdentifier*>(&other);
        return otherUser && otherUser->userId == userId;
    }
};

struct MachineIdentifier : public Identifier {
    int machineId =0;
    bool operator==(const Identifier&other) const override {
        const MachineIdentifier *otherMachine = dynamic_cast<const MachineIdentifier*>(&other);
        return otherMachine && otherMachine->machineId == machineId;
    }
};

int main() {
    UserIdentifier user;
    MachineIdentifier machine;
    return user==machine? 1: 0;
}

https://godbolt.org/z/er4fsK

Nous migrons maintenant vers GCC-10 et clang-10, mais pour des raisons, nous devons encore travailler sur les versions 9 (enfin, au moins clang-9 car c'est ce que Android NDK a actuellement).

Le code ci-dessus arrête la compilation car de nouvelles règles sur les opérateurs de comparaison sont implémentées. L'opérateur réversible == provoque des ambiguïtés. Je ne peux pas utiliser un opérateur de vaisseau spatial car il n'est pas implémenté dans les versions 9. Mais j'ai omis ceci de l'exemple - je suppose que tout ce qui fonctionne avec == fonctionnera avec d'autres opérateurs.

Donc: Quelle est l'approche recommandée pour implémenter des opérateurs de comparaison en C ++ 20 avec des types polymorphes?

Réponses

19 dfrib Oct 27 2020 at 04:13

En tant que solution intermédiaire, vous pouvez re-factoriser votre égalité polymorphe operator==en un non-virtuel operator==défini dans la classe de base, qui se distribue de manière polymorphe à une fonction membre virtuelle non-opérateur:

struct Identifier {    
    bool operator==(const Identifier& other) const {
        return isEqual(other);
    }
private:
    virtual bool isEqual(const Identifier& other) const = 0;
};

// Note: do not derive this class further (less dyncasts may logically fail).
struct UserIdentifier final : public Identifier {
    int userId = 0;
private:
    virtual bool isEqual(const Identifier& other) const override {
        const UserIdentifier *otherUser = dynamic_cast<const UserIdentifier*>(&other);
        return otherUser && otherUser->userId == userId;
    }
};

// Note: do not derive this class further (less dyncasts may logically fail).
struct MachineIdentifier final : public Identifier {
    int machineId = 0;
private:
    virtual bool isEqual(const Identifier& other) const override {
        const MachineIdentifier *otherMachine = dynamic_cast<const MachineIdentifier*>(&other);
        return otherMachine && otherMachine->machineId == machineId;
    }
};

Il n'y aura plus d'ambiguïté puisque la répartition sur la isEqualfonction membre virtuelle sera toujours effectuée sur l'argument de gauche de operator==.

const bool result = (user == machine);  // user.isEqual(machine);
1 goodvibration Oct 27 2020 at 16:10

OK, je vois que cela n'a pas été mentionné dans la réponse donnée par @dfrib, donc je vais étendre cette réponse pour le montrer.

Vous pouvez ajouter une fonction abstraite (virtuelle pure) dans la Identifierstructure, qui renvoie son "identité".

Ensuite, dans chaque structure qui étend la Identifierstructure, vous pouvez appeler cette fonction au lieu de lancer dynamiquement l'objet d'entrée et vérifier que son type correspond à l' thisobjet.

Bien entendu, vous devrez assurer une distinction complète entre l'ensemble des identités de chaque structure. En d'autres termes, deux ensembles d'identités ne doivent pas partager de valeurs communes (c'est-à-dire que les deux ensembles doivent être disjoints).

Cela vous permettra de vous débarrasser complètement de RTTI, qui est à peu près le contraire du polymorphisme IMO, et a également un impact d'exécution supplémentaire en plus de cela.

Voici l'extension de cette réponse:

struct Identifier {    
    bool operator==(const Identifier& other) const {
        return getVal() == other.getVal();
    }
private:
    virtual int getVal() const = 0;
};

struct UserIdentifier : public Identifier {
private:
    int userId = 0;
    virtual int getVal() const override {
        return userId;
    }
};

struct MachineIdentifier : public Identifier {
private:
    int machineId = 100;
    virtual int getVal() const override {
        return machineId;
    }
};

Si vous souhaitez prendre en charge une structure avec des identificateurs d'un autre type que int, vous pouvez étendre cette solution pour utiliser des modèles.

Sinon, pour appliquer un ensemble d'identités différent pour chaque structure, vous pouvez ajouter un typechamp et vous assurer que seul ce champ est unique dans différentes structures.

Essentiellement, ces types seraient l'équivalent de la dynamic_castvérification, qui compare le pointeur de la V-table de l' objet d' entrée et le pointeur de la V-table de la structure d' entrée (d'où mon opinion sur cette approche étant le contraire polymorphisme).

Voici la réponse révisée:

struct Identifier {    
    bool operator==(const Identifier& other) const {
        return getType() == other.getType() && getVal() == other.getVal();
    }
private:
    virtual int getType() const = 0;
    virtual int getVal() const = 0;
};

struct UserIdentifier : public Identifier {
private:
    int userId = 0;
    virtual int getType() const override {
        return 1;
    virtual int getVal() const override {
        return userId;
    }
};

struct MachineIdentifier : public Identifier {
private:
    int machineId = 0;
    virtual int getType() const override {
        return 2;
    virtual int getVal() const override {
        return machineId;
    }
};
1 trentcl Oct 28 2020 at 06:23

Cela ne ressemble pas à un problème de polymorphisme. En fait, je pense que le polymorphisme est le symptôme d'une erreur de modèle de données.

Si vous avez des valeurs qui identifient les machines et des valeurs qui identifient les utilisateurs, et que ces identificateurs ne sont pas interchangeables¹, ils ne doivent pas partager un supertype. La propriété «d'être un identificateur» est un fait sur la façon dont le type est utilisé dans le modèle de données pour identifier des valeurs d'un autre type. A MachineIdentifierest un identifiant car il identifie une machine; a UserIdentifierest un identifiant car il identifie un utilisateur. Mais an Identifiern'est en fait pas un identifiant, car il n'identifie rien! C'est une abstraction brisée.

Une manière plus intuitive de mettre cela pourrait être: le type est la seule chose qui rend un identifiant significatif. Vous ne pouvez rien faire avec un nu Identifier, à moins de l'avoir d'abord abattu MachineIdentifierou UserIdentifier. Donc, avoir une Identifierclasse est probablement une erreur, et comparer a MachineIdentifierà a UserIdentifierest une erreur de type qui devrait être détectée par le compilateur.

Il me semble que la raison la plus probable Identifierexiste parce que quelqu'un s'est rendu compte qu'il y avait du code commun entre MachineIdentifieret UserIdentifier, et a sauté à la conclusion que le comportement commun devrait être extrait vers un Identifiertype de base, les types spécifiques en héritant. C'est une erreur compréhensible pour quiconque a appris à l'école que "l'héritage permet la réutilisation du code" et n'a pas encore réalisé qu'il existe d'autres types de réutilisation du code.

Qu'auraient-ils dû écrire à la place? Que diriez-vous d'un modèle? Les instanciations de modèle ne sont pas des sous-types du modèle ou les uns des autres. Si vous avez des types Machineet Userque ces identificateurs représentent, vous pouvez essayer d'écrire une Identifierstructure de modèle et de la spécialiser, au lieu de la sous-classer:

template <typename T>
struct Identifier {};

template <>
struct Identifier<User> {
  int userId = 0;
  bool operator==(const Identifier<User> &other) const {
    return other.userId == userId;
  }
};

template <>
struct Identifier<Machine> {
  int machineId = 0;
  bool operator==(const Identifier<Machine> &other) const {
    return other.machineId == machineId;
  }
};

Cela a probablement plus de sens lorsque vous pouvez déplacer toutes les données et tous les comportements dans le modèle et que vous n'avez donc pas besoin de vous spécialiser. Sinon, ce n'est pas nécessairement la meilleure option car vous ne pouvez pas spécifier que les Identifierinstanciations doivent être implémentées operator==. Je pense qu'il peut y avoir un moyen d'y parvenir, ou quelque chose de similaire, en utilisant les concepts C ++ 20, mais à la place, combinons les modèles avec l'héritage pour obtenir certains avantages des deux:

template <typename Id>
struct Identifier {
  virtual bool operator==(const Id &other) const = 0;
};

struct UserIdentifier : public Identifier<UserIdentifier> {
  int userId = 0;
  bool operator==(const UserIdentifier &other) const override {
    return other.userId == userId;
  }
};

struct MachineIdentifier : public Identifier<MachineIdentifier> {
  int machineId = 0;
  bool operator==(const MachineIdentifier &other) const override {
    return other.machineId == machineId;
  }
};

Maintenant, comparer a MachineIdentifierà a UserIdentifierest une erreur de compilation.

Cette technique est appelée le modèle de modèle curieusement récurrent (voir aussi crtp ). C'est quelque peu déroutant quand vous le rencontrez pour la première fois, mais ce que cela vous donne, c'est la possibilité de faire référence au type de sous-classe spécifique dans la superclasse (dans cet exemple, comme Id). Cela peut également être une bonne option pour vous car, par rapport à la plupart des autres options, cela nécessite relativement peu de modifications du code qui utilise déjà correctement MachineIdentifieret UserIdentifier.


¹ Si les identificateurs sont interchangeables, alors la plupart de cette réponse (et la plupart des autres réponses) ne s'applique probablement pas. Mais si tel est le cas, il devrait également être possible de les comparer sans abattre.

scohe001 Oct 27 2020 at 04:12

Vous n'avez aucun polymorphisme dans votre code. Vous pouvez forcer une liaison dynamique de la fonction d'opérateur de comparaison (polymorphisme) à l'aide de Identifierpointeurs ou de références.

Par exemple, au lieu de

UserIdentifier user;
MachineIdentifier machine;
return user==machine? 1: 0;

Avec des références, vous pouvez faire:

UserIdentifier user;
MachineIdentifier machine;
Identifier &iUser = user;

return iUser == machine ? 1: 0;

Inversement, vous pouvez appeler explicitement l' UserIdentifieropérateur de comparaison de:

return user.operator==(machine) ? 1: 0;