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 ~VirtuallyDestructible() = 0
un héritage virtuel des classes d'interface (avec des membres abstraits, uniquement). Vous pourriez omettre ce VirtuallyDestructible, probablement.
virtual void bar() = 0;
par exemple? Cela empêcherait votre interface d'être instanciée.