web-dev-qa-db-ja.com

C ++で基本クラスのコンストラクターと代入演算子を使用する方法は?

コンストラクターと代入演算子のセットを持つクラス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
};
_

関数foo()をオーバーライドするだけの継承クラスDを作成し、他の変更は必要ありません。

ただし、Dには、Bと同じコンストラクターのセット(コピーコンストラクターと代入演算子を含む)が必要です。

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

それらをすべてDで書き換える必要がありますか、それともBのコンストラクタと演算子を使用する方法はありますか? Bのプライベートメンバー変数のすべてにアクセスする必要があるため、特に代入演算子の書き換えを避けたいと思います。

91
Igor Oks

コンストラクターと割り当て演算子を明示的に呼び出すことができます。

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

短い回答:はい、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 */
    }
};

XからクラスDを派生した場合でも、このパターンには影響しません。
確かに、基本クラスを明示的に呼び出して少し作業を繰り返す必要がありますが、これは比較的簡単です。

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

デザインに欠陥がある可能性が高い(ヒント:sliceingentity Semanticsvsvalue Semantics )。多態的な階層からのオブジェクトに完全なコピー/値セマンティクスを持つことは、多くの場合まったく必要ありません。後で必要になる可能性がある場合に備えて提供したい場合、それは決して必要ないことを意味します。代わりに基本クラスをコピー不可にします(たとえば、boost :: noncopyableから継承することで)。これですべてです。

そのような必要性reallyが表示される場合の唯一の正しいソリューションは、envelop-letter idiom、または小さなフレームワークですSean ParentとAlexander Stepanov IIRCによるRegular Objectsに関する記事から。他のすべてのソリューションでは、スライシングやLSPで問題が発生します。

また、C++ CoreReference C.67も参照してください。 C.67:基本クラスはコピーを抑制し、「コピー」が必要な場合は代わりに仮想クローンを提供する必要があります

4
Luc Hermitte

デフォルトまたは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が述べたように、コンストラクターを定義すると、コンパイラーはデフォルトのコンストラクターを生成せず、コピーコンストラクターを含みます。

元のコードが間違っています:

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

存在しないメモリを削除します。

1
Mario Galindo