Как использовать конструкторы базового класса и оператор присваивания в С++?

У меня есть класс B с набором конструкторов и оператором присваивания.

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
}

Я хочу создать наследующий класс D, который просто переопределит функцию foo(), и никаких других изменений не потребуется.

Но я хочу, чтобы D имел один и тот же набор конструкторов, включая конструктор копирования и оператор присваивания, как B:

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

Нужно ли переписывать все из них в D или есть способ использовать конструкторы и оператор B? Я хотел бы особо избегать переписывать оператор присваивания, потому что он должен получить доступ ко всем переменным частного члена B.

Ответ 1

Вы можете явно вызвать конструкторы и операторы присваивания:

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

Интересно, что это работает, даже если вы явно не определяете эти функции (тогда он использует сгенерированные компилятором функции).

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

Ответ 2

Короткий ответ: Да, вам нужно будет повторить работу в D

Длинный ответ:

Если ваш производный класс 'D' не содержит новых переменных-членов, то версии по умолчанию (сгенерированные компилятором должны работать просто отлично). Конструктор копирования по умолчанию вызовет родительский конструктор копии, а оператор присваивания по умолчанию вызовет оператор родительского назначения.

Но если ваш класс "D" содержит ресурсы, вам нужно будет выполнить некоторую работу.

Я нахожу ваш конструктор копирования немного странным:

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

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

Обычно копируйте цепочку конструкторов так, чтобы они были скопированы из базы вверх. Здесь, поскольку вы вызываете оператор присваивания, конструктор копирования должен вызвать конструктор по умолчанию, который по умолчанию сначала инициализирует объект снизу вверх. Затем вы снова спускаетесь с помощью оператора присваивания. Это кажется довольно неэффективным.

Теперь, если вы выполняете задание, вы копируете снизу вверх (или сверху вниз), но вам сложно сделать это и обеспечить надежную гарантию исключения. Если в какой-либо момент ресурс не может скопировать и вы выбрали исключение, объект будет находиться в неопределенном состоянии (что плохо).

Обычно я видел, как это делалось наоборот.
Оператор присваивания определяется в терминах конструктора копирования и свопинга. Это связано с тем, что упростить предоставление надежной гарантии исключения. Я не думаю, что вы сможете обеспечить сильную гарантию, делая это так (я могу ошибаться).

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

Даже если вы получаете класс D из X, это не влияет на этот шаблон.
По общему признанию, вам нужно немного повторить работу, сделав явные вызовы в базовый класс, но это относительно тривиально.

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 copy constructor
      // Do most of D 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 */
    }
};

Ответ 3

У вас, скорее всего, есть недостаток в вашем дизайне (подсказка: срез, семантика семантики и семантика значений). Наличие полной семантики copy/value для объекта из полиморфной иерархии часто вовсе не является необходимостью. Если вы хотите предоставить его на случай, если он понадобится позже, значит, вам это не понадобится. Сделать базовый класс несовместимым вместо этого (наследуя от boost:: noncopyable, например), и все.

Единственными правильными решениями, когда возникает необходимость действительно, является идиома огибающей письма или небольшая рамка из статьи о регулярных объектах Шона Ротерна и Александра Степанова IIRC. Все другие решения дадут вам проблемы с нарезкой и/или LSP.

Ответ 4

Вам придется переопределить все конструкторы, которые не являются конструкторами по умолчанию или копиями. Вам не нужно переопределять конструктор копирования и оператор присваивания, поскольку те, которые предоставляются компилятором (в соответствии со стандартом), будут вызывать все базовые версии:

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

Обратите внимание, что, как отметил sbi, если вы определяете какой-либо конструктор, компилятор не будет генерировать конструктор по умолчанию для вас, и он включает конструктор копирования.

Ответ 5

Исходный код неверен:

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

В общем, вы не можете определить конструктор копирования в терминах назначения копии, потому что назначение копии должно освобождать ресурсы, а конструктор копирования - нет.

Чтобы понять это, рассмотрите:

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

Чтобы избежать утечки памяти, назначение копии сначала ДОЛЖНО удалять память, отмеченную 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 !!!
}

Таким образом, конструктор копирования и назначение копии различаются, потому что прежняя конструкция и объект в инициализированной памяти и, позднее, ДОЛЖНЫ сначала освободить существующую память перед конструированием нового объекта.

Если вы сделаете то, что изначально было предложено в этой статье:

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

вы удалите неиспользуемую память.