J'ai une classe B
avec un ensemble de constructeurs et un opérateur d'affectation.
C'est ici:
class B
{
public:
B();
B(const string& s);
B(const B& b) { (*this) = b; }
B& operator=(const B & b);
private:
virtual void foo();
// and other private member variables and functions
};
Je veux créer une classe héritée D
qui remplacera simplement la fonction foo()
, et aucune autre modification n'est requise.
Mais, je veux que D
ait le même ensemble de constructeurs, y compris le constructeur de copie et l'opérateur d'affectation que B
:
D(const D& d) { (*this) = d; }
D& operator=(const D& d);
Dois-je tous les réécrire dans D
, ou existe-t-il un moyen d'utiliser les constructeurs et l'opérateur de B
? Je voudrais surtout éviter de réécrire l'opérateur d'affectation car il doit accéder à toutes les variables de membre privé de B
.
Vous pouvez appeler explicitement des constructeurs et des opérateurs d'affectation:
class Base {
//...
public:
Base(const Base&) { /*...*/ }
Base& operator=(const Base&) { /*...*/ }
};
class Derived : public Base
{
int additional_;
public:
Derived(const Derived& d)
: Base(d) // dispatch to base copy constructor
, additional_(d.additional_)
{
}
Derived& operator=(const Derived& d)
{
Base::operator=(d);
additional_ = d.additional_;
return *this;
}
};
La chose intéressante est que cela fonctionne même si vous n'avez pas défini explicitement ces fonctions (il utilise ensuite les fonctions générées par le compilateur).
class ImplicitBase {
int value_;
// No operator=() defined
};
class Derived : public ImplicitBase {
const char* name_;
public:
Derived& operator=(const Derived& d)
{
ImplicitBase::operator=(d); // Call compiler generated operator=
name_ = strdup(d.name_);
return *this;
}
};
Réponse courte: Oui, vous devrez répéter le travail en D
Longue réponse:
Si votre classe dérivée 'D' ne contient aucune nouvelle variable membre, alors les versions par défaut (générées par le compilateur devraient fonctionner très bien). Le constructeur de copie par défaut appellera le constructeur de copie parent et l'opérateur d'affectation par défaut appellera l'opérateur d'affectation parent.
Mais si votre classe "D" contient des ressources, vous devrez faire un peu de travail.
Je trouve votre constructeur de copie un peu étrange:
B(const B& b){(*this) = b;}
D(const D& d){(*this) = d;}
Normalement, les constructeurs copient la chaîne de manière à ce qu'ils soient copiés à partir de la base. Ici, car vous appelez l'opérateur d'affectation, le constructeur de copie doit appeler le constructeur par défaut pour initialiser par défaut l'objet de bas en haut en premier. Ensuite, vous redescendez en utilisant l'opérateur d'affectation. Cela semble plutôt inefficace.
Maintenant, si vous effectuez une tâche, vous copiez de bas en haut (ou de haut en bas), mais il vous semble difficile de le faire et de fournir une garantie d'exception solide. Si à un moment donné une ressource ne parvient pas à copier et que vous lancez une exception, l'objet sera dans un état indéterminé (ce qui est une mauvaise chose).
Normalement, je l'ai vu faire l'inverse.
L'opérateur d'affectation est défini en termes de constructeur de copie et d'échange. En effet, il est plus facile de fournir la garantie d'exception forte. Je ne pense pas que vous serez en mesure de fournir une garantie solide en procédant de cette façon (je peux me tromper).
class X
{
// If your class has no resources then use the default version.
// Dynamically allocated memory is a resource.
// If any members have a constructor that throws then you will need to
// write your owen version of these to make it exception safe.
X(X const& copy)
// Do most of the work here in the initializer list
{ /* Do some Work Here */}
X& operator=(X const& copy)
{
X tmp(copy); // All resource all allocation happens here.
// If this fails the copy will throw an exception
// and 'this' object is unaffected by the exception.
swap(tmp);
return *this;
}
// swap is usually trivial to implement
// and you should easily be able to provide the no-throw guarantee.
void swap(X& s) throws()
{
/* Swap all members */
}
};
Même si vous dérivez une classe D de X, cela n'affecte pas ce modèle.
Certes, vous devez répéter un peu le travail en faisant des appels explicites dans la classe de base, mais c'est relativement trivial.
class D: public X
{
// Note:
// If D contains no members and only a new version of foo()
// Then the default version of these will work fine.
D(D const& copy)
:X(copy) // Chain X's copy constructor
// Do most of D's work here in the initializer list
{ /* More here */}
D& operator=(D const& copy)
{
D tmp(copy); // All resource all allocation happens here.
// If this fails the copy will throw an exception
// and 'this' object is unaffected by the exception.
swap(tmp);
return *this;
}
// swap is usually trivial to implement
// and you should easily be able to provide the no-throw guarantee.
void swap(D& s) throws()
{
X::swap(s); // swap the base class members
/* Swap all D members */
}
};
Vous avez très probablement un défaut dans votre conception (indice: découpage, sémantique d'entité vs sémantique de valeur). Avoir une copie complète/ sémantique de valeur sur un objet d'une hiérarchie polymorphe n'est souvent pas du tout un besoin. Si vous souhaitez le fournir au cas où vous en auriez besoin plus tard, cela signifie que vous n'en aurez jamais besoin. Rendez la classe de base non copiable à la place (en héritant de boost :: noncopyable par exemple), et c'est tout.
Les seules solutions correctes lorsqu'un tel besoin apparaît réellement sont le idiome de la lettre enveloppe, ou le petit cadre de l'article on Regular Objects par Sean Parent et Alexander Stepanov IIRC. Toutes les autres solutions vous poseront des problèmes de découpe et/ou de LSP.
Sur le sujet, voir aussi C++ CoreReference C.67: C.67: Une classe de base devrait supprimer la copie et fournir un clone virtuel à la place si la "copie" est souhaitée .
Vous devrez redéfinir tous les constructeurs qui ne sont pas des constructeurs par défaut ou copier. Vous n'avez pas besoin de redéfinir le constructeur de copie ni l'opérateur d'affectation car ceux fournis par le compilateur (selon la norme) appellent toutes les versions de la base:
struct base
{
base() { std::cout << "base()" << std::endl; }
base( base const & ) { std::cout << "base(base const &)" << std::endl; }
base& operator=( base const & ) { std::cout << "base::=" << std::endl; }
};
struct derived : public base
{
// compiler will generate:
// derived() : base() {}
// derived( derived const & d ) : base( d ) {}
// derived& operator=( derived const & rhs ) {
// base::operator=( rhs );
// return *this;
// }
};
int main()
{
derived d1; // will printout base()
derived d2 = d1; // will printout base(base const &)
d2 = d1; // will printout base::=
}
Notez que, comme l'a noté sbi, si vous définissez un constructeur, le compilateur ne générera pas le constructeur par défaut pour vous et cela inclut le constructeur de copie.
Le code d'origine est incorrect:
class B
{
public:
B(const B& b){(*this) = b;} // copy constructor in function of the copy assignment
B& operator= (const B& b); // copy assignment
private:
// private member variables and functions
};
En général, vous ne pouvez pas définir le constructeur de copie en termes d'affectation de copie, car l'affectation de copie doit libérer les ressources et pas le constructeur de copie !!!
Pour comprendre cela, considérez:
class B
{
public:
B(Other& ot) : ot_p(new Other(ot)) {}
B(const B& b) {ot_p = new Other(*b.ot_p);}
B& operator= (const B& b);
private:
Other* ot_p;
};
Pour éviter une fuite de mémoire, l'affectation de copie DOIT d'abord supprimer la mémoire pointée par ot_p:
B::B& operator= (const B& b)
{
delete(ot_p); // <-- This line is the difference between copy constructor and assignment.
ot_p = new Other(*b.ot_p);
}
void f(Other& ot, B& b)
{
B b1(ot); // Here b1 is constructed requesting memory with new
b1 = b; // The internal memory used in b1.op_t MUST be deleted first !!!
}
Ainsi, le constructeur de copie et l'affectation de copie sont différents parce que l'ancienne construction et l'objet dans une mémoire initialisée et, plus tard, DOIVENT d'abord libérer la mémoire existante avant de construire le nouvel objet.
Si vous faites ce qui est initialement suggéré dans cet article:
B(const B& b){(*this) = b;} // copy constructor
vous supprimerez une mémoire inexistante.