Remarque: ce qui suit est du code C ++ 03, mais nous nous attendons à un passage à C ++ 11 au cours des deux prochaines années, nous devons donc garder cela à l'esprit.
J'écris une directive (pour les débutants, entre autres) sur la façon d'écrire une interface abstraite en C ++. J'ai lu les deux articles de Sutter sur le sujet, recherché des exemples et des réponses sur Internet et fait quelques tests.
Ce code ne doit PAS être compilé!
void foo(SomeInterface & a, SomeInterface & b)
{
SomeInterface c ; // must not be default-constructible
SomeInterface d(a); // must not be copy-constructible
a = b ; // must not be assignable
}
Tous les comportements ci-dessus trouvent la source de leur problème dans le découpage : L'interface abstraite (ou la classe non-feuille dans la hiérarchie) ne doit pas être constructible ni copiable / assignable, MÊME si la classe dérivée peut l'être.
0ème solution: l'interface de base
class VirtuallyDestructible
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Cette solution est simple et quelque peu naïve: elle échoue à toutes nos contraintes: elle peut être construite par défaut, copiée et copiée (je ne suis même pas sûr de déplacer les constructeurs et les affectations, mais j'ai encore 2 ans pour comprendre IT out).
- Nous ne pouvons pas déclarer le destructeur pur virtuel parce que nous devons le garder en ligne, et certains de nos compilateurs ne digéreront pas les méthodes virtuelles pures avec un corps vide en ligne.
- Oui, le seul point de cette classe est de rendre les implémenteurs virtuellement destructibles, ce qui est un cas rare.
- Même si nous avions une méthode pure virtuelle supplémentaire (ce qui est la majorité des cas), cette classe serait toujours attribuable à la copie.
Donc non...
1ère solution: boost :: non copiable
class VirtuallyDestructible : boost::noncopyable
{
public :
virtual ~VirtuallyDestructible() {}
} ;
Cette solution est la meilleure, car elle est simple, claire et C ++ (pas de macros)
Le problème est qu'il ne fonctionne toujours pas pour cette interface spécifique car VirtuallyConstructible peut toujours être construit par défaut .
- Nous ne pouvons pas déclarer le destructeur pur virtuel car nous devons le garder en ligne, et certains de nos compilateurs ne le digéreront pas.
- Oui, le seul point de cette classe est de rendre les implémenteurs virtuellement destructibles, ce qui est un cas rare.
Un autre problème est que les classes implémentant l'interface non copiable doivent alors déclarer / définir explicitement le constructeur de copie et l'opérateur d'affectation s'ils ont besoin de ces méthodes (et dans notre code, nous avons des classes de valeur auxquelles notre client peut toujours accéder via interfaces).
Cela va à l'encontre de la règle du zéro, c'est là que nous voulons aller: si l'implémentation par défaut est correcte, alors nous devrions pouvoir l'utiliser.
2ème solution: faites-les protégés!
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
// With C++11, these methods would be "= default"
MyInterface() {}
MyInterface(const MyInterface & ) {}
MyInterface & operator = (const MyInterface & ) { return *this ; }
} ;
Ce modèle suit les contraintes techniques que nous avions (au moins dans le code utilisateur): MyInterface ne peut pas être construit par défaut, ne peut pas être construit par copie et ne peut pas être attribué par copie.
De plus, il n'impose aucune contrainte artificielle à l'implémentation de classes , qui sont alors libres de suivre la règle de zéro, ou même de déclarer quelques constructeurs / opérateurs comme "= défaut" en C ++ 11/14 sans problème.
Maintenant, c'est assez verbeux, et une alternative serait d'utiliser une macro, quelque chose comme:
class MyInterface
{
public :
virtual ~MyInterface() {}
protected :
DECLARE_AS_NON_SLICEABLE(MyInterface) ;
} ;
Le protégé doit rester en dehors de la macro (car il n'a pas de portée).
Correctement «espacée» (c'est-à-dire précédée du nom de votre entreprise ou produit), la macro doit être inoffensive.
Et l'avantage est que le code est factorisé dans une seule source, au lieu d'être copié-collé dans toutes les interfaces. Si le constructeur de déplacement et l'affectation de déplacement devaient être explicitement désactivés de la même manière à l'avenir, ce serait un changement très léger dans le code.
Conclusion
- Suis-je paranoïaque pour vouloir que le code soit protégé contre le découpage dans les interfaces? (Je crois que non, mais on ne sait jamais ...)
- Quelle est la meilleure solution parmi celles ci-dessus?
- Y a-t-il une autre meilleure solution?
N'oubliez pas qu'il s'agit d'un modèle qui servira de guide pour les débutants (entre autres), donc une solution comme: "Chaque cas devrait avoir sa mise en œuvre" n'est pas une solution viable.
Bounty et résultats
J'ai accordé la prime à coredump en raison du temps passé à répondre aux questions et de la pertinence des réponses.
Ma solution au problème ira probablement à quelque chose comme ça:
class MyInterface
{
DECLARE_CLASS_AS_INTERFACE(MyInterface) ;
public :
// the virtual methods
} ;
... avec la macro suivante:
#define DECLARE_CLASS_AS_INTERFACE(ClassName) \
public : \
virtual ~ClassName() {} \
protected : \
ClassName() {} \
ClassName(const ClassName & ) {} \
ClassName & operator = (const ClassName & ) { return *this ; } \
private :
Il s'agit d'une solution viable à mon problème pour les raisons suivantes:
- Cette classe ne peut pas être instanciée (les constructeurs sont protégés)
- Cette classe peut être virtuellement détruite
- Cette classe peut être héritée sans imposer de contraintes indues sur les classes héritées (par exemple, la classe héritée pourrait être copiable par défaut)
- L'utilisation de la macro signifie que la "déclaration" de l'interface est facilement reconnaissable (et consultable), et son code est factorisé en un seul endroit, ce qui le rend plus facile à modifier (un nom convenablement préfixé supprimera les conflits de noms indésirables)
Notez que les autres réponses ont donné un aperçu précieux. Merci à tous ceux qui ont essayé.
Notez que je suppose que je peux encore mettre une autre prime sur cette question, et j'apprécie suffisamment les réponses éclairantes pour que si j'en vois une, j'ouvrirais une prime juste pour l'attribuer à cette réponse.
virtual void bar() = 0;
par exemple? Cela empêcherait votre interface d'être instanciée.virtual ~VirtuallyDestructible() = 0
un héritage virtuel des classes d'interface (avec des membres abstraits, uniquement). Vous pourriez omettre ce VirtuallyDestructible, probablement.Réponses:
La manière canonique de créer une interface en C ++ est de lui donner un destructeur virtuel pur. Cela garantit que
delete
un pointeur sur l'interface fait la bonne chose: il appelle le destructeur de la classe la plus dérivée pour cette instance.le simple fait d'avoir un destructeur virtuel pur n'empêche pas l'affectation sur une référence à l'interface. Si vous avez également besoin que cela échoue, vous devez ajouter un opérateur d'affectation protégé à votre interface.
Tout compilateur C ++ devrait être capable de gérer une classe / interface comme celle-ci (le tout dans un fichier d'en-tête):
Si vous avez un compilateur qui s'étouffe (ce qui signifie qu'il doit être pré-C ++ 98), alors votre option 2 (ayant des constructeurs protégés) est une bonne seconde.
L'utilisation
boost::noncopyable
n'est pas recommandée pour cette tâche, car elle envoie le message que toutes les classes de la hiérarchie doivent être non copiables et cela peut donc créer de la confusion pour les développeurs plus expérimentés qui ne connaissent pas vos intentions de l'utiliser comme ça.la source
If you need [prevent assignment] to fail as well, then you must add a protected assignment operator to your interface.
: Ceci est la racine de mon problème. Les cas où j'ai besoin d'une interface pour prendre en charge l'affectation doivent être rares en effet. En revanche, les cas où je veux passer une interface par référence (les cas où NULL n'est pas acceptable), et donc, veulent éviter un no-op ou un découpage qui se compilent sont beaucoup plus importants.private
? De plus, vous voudrez peut-être traiter les paramètres par défaut et de copie.Suis-je paranoïaque ...
N'est-ce pas un problème de gestion des risques?
Meilleure solution
Votre deuxième solution ("les rendre protégés") semble bonne, mais gardez à l'esprit que je ne suis pas un expert en C ++.
Au moins, les utilisations invalides semblent être correctement signalées comme erronées par mon compilateur (g ++).
Maintenant, avez-vous besoin de macros? Je dirais «oui», car même si vous ne dites pas quel est le but de la directive que vous écrivez, je suppose que c'est pour appliquer un ensemble particulier de meilleures pratiques dans le code de votre produit.
À cet effet, les macros peuvent aider à détecter quand les gens appliquent efficacement le modèle: un filtre de base des validations peut vous dire si la macro a été utilisée:
protected
mot - clé),Sans macros, vous devez vérifier si le modèle est nécessaire et bien implémenté dans tous les cas.
Meilleure solution
Le découpage en C ++ n'est rien de plus qu'une particularité du langage. Puisque vous écrivez une directive (en particulier pour les débutants), vous devez vous concentrer sur l'enseignement et non pas seulement sur l'énumération des "règles de codage". Vous devez vous assurer que vous expliquez vraiment comment et pourquoi le découpage se produit, ainsi que des exemples et des exercices (ne réinventez pas la roue, inspirez-vous des livres et des tutoriels).
Par exemple, le titre d'un exercice pourrait être " Quel est le modèle d'une interface sûre en C ++ ?"
Ainsi, votre meilleure décision serait de vous assurer que vos développeurs C ++ comprennent ce qui se passe lors du découpage. Je suis convaincu que s'ils le font, ils ne feront pas autant d'erreurs dans le code que vous le craindriez, même sans appliquer formellement ce modèle particulier (mais vous pouvez toujours le faire appliquer, les avertissements du compilateur sont bons).
À propos du compilateur
Vous dites :
Souvent, les gens disent "je n'ai pas le droit de faire [X]" , "je ne suis pas censé faire [Y] ..." , ... parce qu'ils pensent que ce n'est pas possible, et non parce qu'ils essayé ou demandé.
Cela fait probablement partie de votre description de poste de donner votre avis sur des questions techniques; si vous pensez vraiment que le compilateur est le choix parfait (ou unique) pour votre domaine problématique, alors utilisez-le. Mais vous avez également dit que "les destructeurs virtuels purs avec implémentation en ligne ne sont pas le pire point d'étouffement que j'ai vu" ; d'après ma compréhension, le compilateur est si spécial que même les développeurs C ++ compétents ont des difficultés à l'utiliser: votre compilateur hérité / interne est maintenant une dette technique, et vous avez le droit (le devoir?) de discuter de ce problème avec d'autres développeurs et gestionnaires .
Essayez d'évaluer le coût de conservation du compilateur par rapport au coût d'utilisation d'un autre:
Je ne connais pas votre situation, et en fait, vous avez probablement des raisons valables d'être lié à un compilateur spécifique.
Mais dans le cas où il ne s'agit que d'une simple inertie, la situation n'évoluera jamais si vous ou vos collègues ne signalez pas de problèmes de productivité ou de dette technique.
la source
Am I paranoid...
: "Rendez vos interfaces faciles à utiliser correctement et difficiles à utiliser incorrectement". J'ai goûté à ce principe particulier lorsque quelqu'un a signalé qu'une de mes méthodes statiques était, par erreur, mal utilisée. L'erreur produite ne semblait pas liée et il a fallu plusieurs heures à un ingénieur pour trouver la source. Cette "erreur d'interface" est comparable à l'attribution d'une référence d'interface à une autre. Donc, oui, je veux éviter ce genre d'erreur. De plus, en C ++, la philosophie est de capturer autant que possible au moment de la compilation, et le langage nous donne ce pouvoir, alors nous allons avec.Best solution
: Je suis d'accord. . .Better solution
: C'est une réponse géniale. Je vais y travailler ... Maintenant, à propos dePure virtual classes
: Qu'est-ce que c'est? Une interface abstraite C ++? (classe sans état et uniquement des méthodes virtuelles pures?). Comment cette «classe virtuelle pure» m'a protégé contre le découpage? (les méthodes virtuelles pures feront que l'instanciation ne se compilera pas, mais l'affectation de copie le fera et l'affectation de déplacement le sera aussi IIRC).About the compiler
: Nous sommes d'accord, mais nos compilateurs n'entrent pas dans mon champ de responsabilité (pas que cela m'empêche de commentaires sournois ... :-p ...). Je ne divulguerai pas les détails (j'aimerais pouvoir le faire) mais cela est lié à des raisons internes (comme les suites de tests) et à des raisons externes (par exemple, le client se connectant à nos bibliothèques). En fin de compte, changer la version du compilateur (ou même le patcher) n'est PAS une opération triviale. Sans parler de remplacer un compilateur cassé par un gcc récent.Le problème du découpage en tranches est un, mais certainement pas le seul, introduit lorsque vous exposez une interface polymorphe d'exécution à vos utilisateurs. Pensez aux pointeurs nuls, à la gestion de la mémoire, aux données partagées. Aucun de ces problèmes n'est facilement résolu dans tous les cas (les pointeurs intelligents sont excellents, mais même ils ne sont pas une solution miracle). En fait, à partir de votre message, il ne semble pas que vous essayez de résoudre le problème du découpage, mais plutôt de le contourner en ne permettant pas aux utilisateurs de faire des copies. Tout ce que vous devez faire pour proposer une solution au problème de découpage est d'ajouter une fonction de membre de clone virtuel. Je pense que le problème le plus profond avec l'exposition d'une interface polymorphe au moment de l'exécution est que vous forcez les utilisateurs à gérer la sémantique de référence, qui est plus difficile à raisonner que la sémantique de valeur.
La meilleure façon que je connaisse pour éviter ces problèmes en C ++ est d'utiliser l' effacement de type . Il s'agit d'une technique dans laquelle vous masquez une interface polymorphe d'exécution, derrière une interface de classe normale. Cette interface de classe normale a alors une sémantique de valeur et s'occupe de tout le «désordre» polymorphe derrière les écrans.
std::function
est un excellent exemple d'effacement de type.Pour une excellente explication des raisons pour lesquelles l'exposition de l'héritage à vos utilisateurs est mauvaise et comment l'effacement de type peut aider à résoudre ce problème, voyez ces présentations par Sean Parent:
L'hérédité est la classe de base du mal (version courte)
Polymorphisme basé sur la sémantique et les concepts de valeur (version longue; plus facile à suivre, mais le son n'est pas excellent)
la source
Tu n'es pas paranoïaque. Ma première tâche professionnelle en tant que programmeur C ++ a entraîné le découpage et le plantage. J'en connais d'autres. Il n'y a pas beaucoup de bonnes solutions pour cela.
Compte tenu de vos contraintes de compilation, l'option 2 est la meilleure. Au lieu de créer une macro, que vos nouveaux programmeurs considéreront comme étrange et mystérieuse, je suggérerais un script ou un outil pour générer automatiquement le code. Si vos nouveaux employés utiliseront un IDE, vous devriez pouvoir créer un outil "Nouvelle interface MYCOMPANY" qui demandera le nom de l'interface et créer la structure que vous recherchez.
Si vos programmeurs utilisent la ligne de commande, utilisez le langage de script disponible pour créer le script NewMyCompanyInterface pour générer le code.
J'ai utilisé cette approche dans le passé pour les modèles de code courants (interfaces, machines à états, etc.). La bonne partie est que les nouveaux programmeurs peuvent lire la sortie et la comprendre facilement, reproduisant le code nécessaire lorsqu'ils ont besoin de quelque chose qui ne peut pas être généré.
Les macros et autres approches de méta-programmation ont tendance à obscurcir ce qui se passe, et les nouveaux programmeurs n'apprennent pas ce qui se passe «derrière le rideau». Quand ils doivent rompre le schéma, ils sont tout aussi perdus qu'auparavant.
la source