Как использовать конструкторы базового класса и оператор присваивания в C ++? - PullRequest
87 голосов
/ 04 августа 2009

У меня есть класс 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.

Ответы [ 5 ]

112 голосов
/ 04 августа 2009

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

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;
    }
};  
17 голосов
/ 04 августа 2009

Краткий ответ: Да, вам нужно будет повторить работу в 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'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 */
    }
};
4 голосов
/ 04 августа 2009

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

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

По теме см. Также C ++ CoreReference C.67: C.67: Базовый класс должен подавлять копирование и предоставлять вместо него виртуальный клон, если требуется «копирование» .

2 голосов
/ 04 августа 2009

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

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, если вы определите какой-либо конструктор, компилятор не сгенерирует для вас конструктор по умолчанию, в который входит конструктор копирования.

1 голос
/ 01 января 2015

Неправильный исходный код:

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

вы будете удалять несуществующую память.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...