Hoe de constructors en de toewijzingsoperator van de basisklasse gebruiken in C++?

Ik heb een klasse Bmet een set constructors en een toewijzingsoperator.

Hier is het:

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

Ik wil een overervende klasse Dmaken die alleen de functie foo()overschrijft, en er is geen andere wijziging vereist.

Maar ik wil dat Ddezelfde set constructors heeft, inclusief kopieerconstructor en toewijzingsoperator als B:

D(const D& d) { (*this) = d; }
D& operator=(const D& d);

Moet ik ze allemaal herschrijven in D, of is er een manier om de constructors en operator van Bte gebruiken? Ik zou vooral willen vermijden om de toewijzingsoperator te herschrijven omdat deze toegang moet hebben tot alle privé-lidvariabelen van B.


Antwoord 1, autoriteit 100%

U kunt constructors en toewijzingsoperators expliciet aanroepen:

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

Het interessante is dat dit werkt, zelfs als je deze functies niet expliciet hebt gedefinieerd (het gebruikt dan de door de compiler gegenereerde functies).

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

Antwoord 2, autoriteit 15%

Kort antwoord: Ja, u moet het werk in D herhalen

Lang antwoord:

Als uw afgeleide klasse ‘D’ geen nieuwe lidvariabelen bevat, dan zouden de standaardversies (gegenereerd door de compiler prima moeten werken). De standaard Copy-constructor roept de parent-copy-constructor aan en de standaard toewijzingsoperator roept de parent-toewijzingsoperator aan.

Maar als je klasse ‘D’ bronnen bevat, moet je wat werk verzetten.

Ik vind je copy-constructor een beetje vreemd:

B(const B& b){(*this) = b;}
D(const D& d){(*this) = d;}

Normaal gesproken kopieert u de keten van constructors zodat ze vanaf het begin gekopieerd zijn. Omdat u hier de toewijzingsoperator aanroept, moet de kopieerconstructor de standaardconstructor aanroepen om het object eerst van onder naar boven te initialiseren. Daarna ga je weer naar beneden met behulp van de toewijzingsoperator. Dit lijkt nogal inefficiënt.

Als je nu een opdracht doet, kopieer je van onder naar boven (of van boven naar beneden), maar het lijkt moeilijk voor je om dat te doen en een sterke uitzonderingsgarantie te bieden. Als een resource op enig moment niet kan kopiëren en u een uitzondering maakt, bevindt het object zich in een onbepaalde status (wat een slechte zaak is).

Normaal gesproken heb ik het andersom gezien.
De toewijzingsoperator wordt gedefinieerd in termen van de kopieerconstructor en swap. Dit komt omdat het gemakkelijker is om de sterke uitzonderingsgarantie te bieden. Ik denk niet dat je de sterke garantie kunt geven door het op deze manier te doen (ik kan het mis hebben).

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

Zelfs als je een klasse D afleidt van X, heeft dit geen invloed op dit patroon.
Toegegeven, je moet een beetje van het werk herhalen door expliciet naar de basisklasse te bellen, maar dit is relatief triviaal.

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

Antwoord 3, autoriteit 2%

U heeft hoogstwaarschijnlijk een fout in uw ontwerp (hint: slicing, entiteitssemantiekversus waardesemantiek). Het hebben van een volledige kopie/waardesemantiekop een object uit een polymorfe hiërarchie is vaak helemaal niet nodig. Als u het wilt verstrekken voor het geval u het later nodig heeft, betekent dit dat u het nooit nodig zult hebben. Maak in plaats daarvan de basisklasse niet kopieerbaar (door bijvoorbeeld over te nemen van boost::noncopyable), en dat is alles.

De enige juiste oplossingen wanneer een dergelijke behoefte echtverschijnt, zijn de envelop-letter idioom, of het kleine kader uit het artikel over Regular Objectsdoor Sean Parent en Alexander Stepanov IIRC. Bij alle andere oplossingen krijg je problemen met slicen en/of de LSP.

Zie over dit onderwerp ook C++CoreReference C.67: C.67: Een basisklasse moet kopiëren onderdrukken en in plaats daarvan een virtuele kloon leveren als “kopiëren” gewenst is.


Antwoord 4

Je zult alle constructors die geen standaardof kopieconstructors zijn, opnieuw moeten definiëren. U hoeft de kopieerconstructor of de toewijzingsoperator niet opnieuw te definiëren, aangezien de compiler (volgens de standaard) alle versies van de basis aanroept:

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::=
}

Merk op dat, zoals sbi opmerkte, als u een constructor definieert, de compiler niet de standaardconstructor voor u zal genereren en dat omvat de kopieerconstructor.


Antwoord 5

De originele code is verkeerd:

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

Over het algemeen kun je de kopieerconstructor niet definiëren in termen van de kopieeropdracht, omdat de kopieeropdracht de bronnen moet vrijgeven en de kopieerconstructor niet!!!

Om dit te begrijpen, overweeg:

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

Om geheugenlek te voorkomen, MOET de kopieeropdracht eerst het geheugen verwijderen dat door ot_p is aangegeven:

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

Dus de kopieerconstructor en de kopieertoewijzing zijn verschillend, omdat de eerste constructie en het object in een geïnitialiseerd geheugen worden geplaatst en de latere eerst het bestaande geheugen MOETEN vrijgeven voordat het nieuwe object wordt geconstrueerd.

Als u doet wat oorspronkelijk in dit artikel wordt gesuggereerd:

B(const B& b){(*this) = b;} // copy constructor

je gaat een niet-bestaande herinnering verwijderen.

Other episodes