wake-up-neo.com

Wie verwende ich die Konstruktoren und Zuweisungsoperatoren der Basisklasse in C ++?

Ich habe eine Klasse B mit einer Reihe von Konstruktoren und einem Zuweisungsoperator.

Hier ist es:

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

Ich möchte eine erbende Klasse D erstellen, die nur die Funktion foo() überschreibt, und es ist keine weitere Änderung erforderlich.

Aber ich möchte, dass D die gleichen Konstruktoren hat, einschließlich des Kopierkonstruktors und des Zuweisungsoperators wie B:

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

Muss ich alle in D umschreiben, oder gibt es eine Möglichkeit, die Konstruktoren und den Operator von B zu verwenden? Ich möchte insbesondere vermeiden, den Zuweisungsoperator neu zu schreiben, da er auf alle privaten Membervariablen von B zugreifen muss.

91
Igor Oks

Sie können Konstruktoren und Zuweisungsoperatoren explizit aufrufen:

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

Das Interessante ist, dass dies auch dann funktioniert, wenn Sie diese Funktionen nicht explizit definiert haben (dann werden die vom Compiler generierten Funktionen verwendet).

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

Kurze Antwort: Ja, Sie müssen die Arbeit in D wiederholen

Lange Antwort:

Wenn Ihre abgeleitete Klasse 'D' keine neuen Mitgliedsvariablen enthält, sollten die vom Compiler generierten Standardversionen einwandfrei funktionieren. Der Standard-Kopierkonstruktor ruft den übergeordneten Kopierkonstruktor auf, und der Standardzuweisungsoperator ruft den übergeordneten Zuweisungsoperator auf.

Wenn Ihre Klasse 'D' Ressourcen enthält, müssen Sie einige Arbeiten ausführen.

Ich finde Ihren Kopierkonstruktor etwas seltsam:

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

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

Normalerweise werden Konstruktoren so verkettet, dass sie von Grund auf kopiert werden. Da Sie hier den Zuweisungsoperator aufrufen, muss der Kopierkonstruktor den Standardkonstruktor aufrufen, um das Objekt standardmäßig von unten nach oben zu initialisieren. Dann gehen Sie mit dem Zuweisungsoperator wieder runter. Dies scheint ziemlich ineffizient zu sein.

Wenn Sie jetzt eine Aufgabe ausführen, kopieren Sie von unten nach oben (oder von oben nach unten), aber es scheint schwierig für Sie zu sein, dies zu tun und eine starke Ausnahmegarantie zu bieten. Wenn eine Ressource zu irgendeinem Zeitpunkt nicht kopiert werden kann und Sie eine Ausnahme auslösen, befindet sich das Objekt in einem unbestimmten Zustand (was eine schlechte Sache ist).

Normalerweise habe ich es umgekehrt gesehen.
Der Zuweisungsoperator wird in Bezug auf den Kopierkonstruktor und den Swap definiert. Dies liegt daran, dass es einfacher ist, eine starke Ausnahmegarantie bereitzustellen. Ich denke nicht, dass Sie in der Lage sein werden, die starke Garantie zu geben, wenn Sie es auf diese Weise tun (ich könnte mich irren).

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

Selbst wenn Sie eine Klasse D von X ableiten, wirkt sich dies nicht auf dieses Muster aus.
Sie müssen zwar einen Teil der Arbeit wiederholen, indem Sie die Basisklasse explizit aufrufen, aber das ist relativ 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 */
    }
};
17
Martin York

Sie haben höchstwahrscheinlich einen Fehler in Ihrem Design (Hinweis: Slicing, Entitätssemantik vs Wertsemantik). Eine vollständige Kopie/ Wertsemantik eines Objekts aus einer polymorphen Hierarchie ist oft überhaupt nicht erforderlich. Wenn Sie es nur für den Fall bereitstellen möchten, dass Sie es später benötigen, bedeutet dies, dass Sie es niemals benötigen werden. Machen Sie die Basisklasse stattdessen nicht kopierbar (indem Sie zum Beispiel von boost :: noncopyable erben), und das ist alles.

Die einzig richtigen Lösungen, wenn ein solches Bedürfnis wirklich auftritt, sind das Umschlagbuchstaben-Idiom oder der kleine Rahmen aus dem Artikel on Regular Objects von Sean Parent und Alexander Stepanov IIRC. Bei allen anderen Lösungen treten Probleme mit dem Schneiden und/oder dem LSP auf.

Siehe hierzu auch C++ CoreReference C.67: C.67: Eine Basisklasse sollte das Kopieren unterdrücken und stattdessen einen virtuellen Klon bereitstellen, wenn "Kopieren" gewünscht wird .

4
Luc Hermitte

Sie müssen alle Konstruktoren neu definieren, die nicht Standard oder Kopie Konstruktoren sind. Sie müssen weder den Kopierkonstruktor noch den Zuweisungsoperator neu definieren, da die vom Compiler (gemäß Standard) bereitgestellten Versionen alle Basisversionen aufrufen:

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

Beachten Sie, dass der Compiler, wie von sbi angegeben, beim Definieren eines Konstruktors nicht den Standardkonstruktor für Sie generiert und den Kopierkonstruktor einschließt.

Der ursprüngliche Code ist falsch:

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

Im Allgemeinen können Sie den Kopierkonstruktor nicht in Bezug auf die Kopierzuordnung definieren, da die Kopierzuordnung die Ressourcen freigeben muss und der Kopierkonstruktor nicht !!!

Um dies zu verstehen, betrachten Sie:

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

Um Speicherverluste zu vermeiden, MUSS die Kopierzuweisung zuerst den Speicher löschen, auf den ot_p zeigt:

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

Daher unterscheiden sich der Kopierkonstruktor und die Kopierzuweisung, da das vorherige Konstrukt und das vorherige Objekt in einem initialisierten Speicher abgelegt werden müssen. In diesem Fall MUSS der vorhandene Speicher zuerst freigegeben werden, bevor das neue Objekt erstellt wird.

Wenn Sie das tun, was ursprünglich in diesem Artikel vorgeschlagen wurde:

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

sie löschen einen nicht vorhandenen Speicher.

1
Mario Galindo