web-dev-qa-db-ja.com

仮想代入演算子C ++

C++の代入演算子は仮想化できます。なぜ必要なのですか?他のオペレーターも仮想化できますか?

65
Kazoom

代入演算子は仮想化する必要はありません。

以下の説明は_operator=_についてですが、問題の型を受け取る演算子のオーバーロード、および問題の型を受け取る関数にも適用されます。

以下の説明は、一致する関数シグネチャの検索に関して、仮想キーワードがパラメーターの継承を知らないことを示しています。最後の例では、継承された型を扱うときに割り当てを適切に処理する方法を示しています。


仮想関数はパラメーターの継承を知らない:

関数のシグネチャは、仮想が機能するために同じである必要があります。そのため、次の例ではoperator =が仮想化されていますが、operator =のパラメーターと戻り値が異なるため、呼び出しはDの仮想関数として機能しません。

関数B::operator=(const B& right)D::operator=(const D& right)は100%完全に異なり、2つの異なる関数として表示されます。

_class B
{
public:
  virtual B& operator=(const B& right)
  {
    x = right.x;
    return *this;
  }

  int x;

};

class D : public B
{
public:
  virtual D& operator=(const D& right)
  {
    x = right.x;
    y = right.y;
    return *this;
  }
  int y;
};
_

デフォルト値と2つのオーバーロードされた演算子:

ただし、仮想関数を定義して、タイプBの変数に割り当てられたDのデフォルト値を設定できるようにすることができます。これは、B変数が実際にBの参照に格納されたDであってもですD::operator=(const D& right)関数。

以下の場合、2 B参照内に格納された2 Dオブジェクトからの割り当て... D::operator=(const B& right)オーバーライドが使用されます。

_//Use same B as above

class D : public B
{
public:
  virtual D& operator=(const D& right)
  {
    x = right.x;
    y = right.y;
    return *this;
  }


  virtual B& operator=(const B& right)
  {
    x = right.x;
    y = 13;//Default value
    return *this;
  }

  int y;
};


int main(int argc, char **argv) 
{
  D d1;
  B &b1 = d1;
  d1.x = 99;
  d1.y = 100;
  printf("d1.x d1.y %i %i\n", d1.x, d1.y);

  D d2;
  B &b2 = d2;
  b2 = b1;
  printf("d2.x d2.y %i %i\n", d2.x, d2.y);
  return 0;
}
_

プリント:

_d1.x d1.y 99 100
d2.x d2.y 99 13
_

D::operator=(const D& right)が使用されないことを示しています。

B::operator=(const B& right)に仮想キーワードがなければ、上記と同じ結果になりますが、yの値は初期化されません。つまりB::operator=(const B& right)を使用します


すべてを結び付ける最後のステップ、RTTI:

RTTIを使用して、タイプを受け取る仮想関数を適切に処理できます。継承された可能性のあるタイプを扱うときに割り当てを適切に処理する方法を理解するためのパズルの最後のピースを次に示します。

_virtual B& operator=(const B& right)
{
  const D *pD = dynamic_cast<const D*>(&right);
  if(pD)
  {
    x = pD->x;
    y = pD->y;
  }
  else
  {
    x = right.x;
    y = 13;//default value
  }

  return *this;
}
_
47
Brian R. Bondy

オペレーターに依存します。

代入演算子を仮想化することのポイントは、それをオーバーライドしてより多くのフィールドをコピーできるという利点を利用できるようにすることです。

したがって、Base&があり、実際に動的タイプとしてDerived&があり、Derivedにさらにフィールドがある場合、正しいものがコピーされます。

ただし、LHSがDerivedであり、RHSがBaseであるというリスクがあるため、仮想オペレーターがDerivedで実行される場合、パラメーターはDerivedではなく、フィールドを取得する方法はありません。

ここに良い議論があります: http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html

24
Uri

ブライアンR.ボンディ 書いた:


RTTI:を結び付ける最後のステップ

RTTIを使用して、タイプを受け取る仮想関数を適切に処理できます。継承された可能性のあるタイプを扱うときに割り当てを適切に処理する方法を理解するためのパズルの最後のピースを次に示します。

virtual B& operator=(const B& right)
{
  const D *pD = dynamic_cast<const D*>(&right);
  if(pD)
  {
    x = pD->x;
    y = pD->y;
  }
  else
  {
    x = right.x;
    y = 13;//default value
  }

  return *this;
}

このソリューションにいくつかのコメントを追加したいと思います。割り当て演算子を上記と同じように宣言することには、3つの問題があります。

コンパイラは、const D&引数をとる代入演算子を生成します。この引数は、仮想ではなく、考えていることを行いません。

2番目の問題は戻り値の型です。派生インスタンスへのベース参照を返しています。とにかくコードが機能するので、おそらくそれほど問題ではありません。それでも、それに応じて参照を返すことをお勧めします。

3番目の問題は、派生型の代入演算子が基本クラスの代入演算子を呼び出さないことです(コピーしたいプライベートフィールドがある場合はどうなりますか?)、代入演算子を仮想として宣言してもコンパイラは生成しません。これは、意図した結果を得るために、割り当て演算子の少なくとも2つのオーバーロードがないという副作用です。

基本クラスを考えます(私が引用した投稿からのものと同じです):

class B
{
public:
    virtual B& operator=(const B& right)
    {
        x = right.x;
        return *this;
    }

    int x;
};

次のコードは、引用したRTTIソリューションを完成させます。

class D : public B{
public:
    // The virtual keyword is optional here because this
    // method has already been declared virtual in B class
    /* virtual */ const D& operator =(const B& b){
        // Copy fields for base class
        B::operator =(b);
        try{
            const D& d = dynamic_cast<const D&>(b);
            // Copy D fields
            y = d.y;
        }
        catch (std::bad_cast){
            // Set default values or do nothing
        }
        return *this;
    }

    // Overload the assignment operator
    // It is required to have the virtual keyword because
    // you are defining a new method. Even if other methods
    // with the same name are declared virtual it doesn't
    // make this one virtual.
    virtual const D& operator =(const D& d){
        // Copy fields from B
        B::operator =(d);
        // Copy D fields
        y = d.y;
        return *this;
    }

    int y;
};

これは完全な解決策に思えるかもしれませんが、そうではありません。これは完全な解決策ではありません。なぜなら、Dから導出する場合、1つの演算子=が必要であり、const B&、1つの演算子=が必要ですconst D&およびconst D2&をとる1つの演算子。結論は明らかです。演算子=()オーバーロードの数は、スーパークラスの数+ 1と同等です。

D2がDを継承することを考慮して、継承された2つのoperator =()メソッドがどのように見えるかを見てみましょう。

class D2 : public D{
    /* virtual */ const D2& operator =(const B& b){
        D::operator =(b); // Maybe it's a D instance referenced by a B reference.
        try{
            const D2& d2 = dynamic_cast<const D2&>(b);
            // Copy D2 stuff
        }
        catch (std::bad_cast){
            // Set defaults or do nothing
        }
        return *this;
    }

    /* virtual */ const D2& operator =(const D& d){
        D::operator =(d);
        try{
            const D2& d2 = dynamic_cast<const D2&>(d);
            // Copy D2 stuff
        }
        catch (std::bad_cast){
            // Set defaults or do nothing
        }
        return *this;
    }
};

operator =(const D2&)はフィールドをコピーするだけで、あたかもそこにあるかのように想像してください。継承された演算子=()オーバーロードのパターンに気付くことができます。悲しいことに、このパターンを処理する仮想テンプレートメソッドを定義することはできません。完全なポリモーフィック割り当て演算子を取得するために、同じコードを複数回コピーして貼り付ける必要があります。これが唯一の解決策です。他の二項演算子にも適用されます。


編集

コメントで述べたように、生活を楽にするためにできることは、一番上のスーパークラス割り当て演算子=()を定義し、他のすべてのスーパークラス演算子=()メソッドから呼び出すことです。また、フィールドをコピーするときに、_copyメソッドを定義できます。

class B{
public:
    // _copy() not required for base class
    virtual const B& operator =(const B& b){
        x = b.x;
        return *this;
    }

    int x;
};

// Copy method usage
class D1 : public B{
private:
    void _copy(const D1& d1){
        y = d1.y;
    }

public:
    /* virtual */ const D1& operator =(const B& b){
        B::operator =(b);
        try{
            _copy(dynamic_cast<const D1&>(b));
        }
        catch (std::bad_cast){
            // Set defaults or do nothing.
        }
        return *this;
    }

    virtual const D1& operator =(const D1& d1){
        B::operator =(d1);
        _copy(d1);
        return *this;
    }

    int y;
};

class D2 : public D1{
private:
    void _copy(const D2& d2){
        z = d2.z;
    }

public:
    // Top-most superclass operator = definition
    /* virtual */ const D2& operator =(const B& b){
        D1::operator =(b);
        try{
            _copy(dynamic_cast<const D2&>(b));
        }
        catch (std::bad_cast){
            // Set defaults or do nothing
        }
        return *this;
    }

    // Same body for other superclass arguments
    /* virtual */ const D2& operator =(const D1& d1){
        // Conversion to superclass reference
        // should not throw exception.
        // Call base operator() overload.
        return D2::operator =(dynamic_cast<const B&>(d1));
    }

    // The current class operator =()
    virtual const D2& operator =(const D2& d2){
        D1::operator =(d2);
        _copy(d2);
        return *this;
    }

    int z;
};

set defaultsメソッドは1つの呼び出し(ベース演算子=()オーバーロードで)のみを受け取るため、必要ありません。フィールドのコピー時の変更は1か所で行われ、すべてのoperator =()オーバーロードが影響を受け、意図した目的を果たします。

sehe 提案をありがとう。

6
Andrei15193

以下のシナリオで仮想割り当てが使用されます:

_//code snippet
Class Base;
Class Child :public Base;

Child obj1 , obj2;
Base *ptr1 , *ptr2;

ptr1= &obj1;
ptr2= &obj2 ;

//Virtual Function prototypes:
Base& operator=(const Base& obj);
Child& operator=(const Child& obj);
_

ケース1:obj1 = obj2;

この仮想概念では、Childクラスで_operator=_を呼び出すため、何の役割も果たしません。

ケース2&3:* ptr1 = obj2;
* ptr1 = * ptr2;

ここでの割り当ては期待どおりではありません。代わりに_operator=_である理由はBaseクラスで呼び出されます。

次のいずれかを使用して修正できます。
1)キャスト

_dynamic_cast<Child&>(*ptr1) = obj2;   // *(dynamic_cast<Child*>(ptr1))=obj2;`
dynamic_cast<Child&>(*ptr1) = dynamic_cast<Child&>(*ptr2)`
_

2)仮想概念

これで、ChildBaseの_operator=_の署名が異なるため、単にvirtual Base& operator=(const Base& obj)を使用しても役に立ちません。

子クラスに通常のBase& operator=(const Base& obj)定義とともにChild& operator=(const Child& obj)を追加する必要があります。デフォルトの代入演算子がない場合は呼び出されるため、後の定義を含めることが重要です(_obj1=obj2_は望ましい結果をもたらさないかもしれません)

_Base& operator=(const Base& obj)
{
    return operator=(dynamic_cast<Child&>(const_cast<Base&>(obj)));
}
_

ケース4:obj1 = * ptr2;

この場合、コンパイラーは、operator=_がChildで呼び出されるため、Childoperator=(Base& obj)定義を探します。しかし、存在せず、Base型をchildに暗黙的に昇格させることはできないため、エラーが発生します(obj1=dynamic_cast<Child&>(*ptr1);のようなキャストが必要です)

Case2&3に従って実装する場合、このシナリオは処理されます。

見てわかるように、仮想割り当ては、ベースクラスのポインタ/参照を使用した割り当ての場合、呼び出しをよりエレガントにします。

他のオペレーターも仮想化できますか? はい

5
sorv3235055

クラスから派生したクラスがすべてのメンバーを正しくコピーすることを保証したい場合にのみ必要です。ポリモーフィズムで何もしていない場合は、実際にこれを心配する必要はありません。

私はあなたが望む演算子を仮想化することを妨げるものは何も知りません-それらは特別な場合のメソッド呼び出しに過ぎません。

このページ は、これらすべてがどのように機能するかについての優れた詳細な説明を提供します。

4
sblom

演算子は、特別な構文を持つメソッドです。他の方法と同じように扱うことができます...

3
dmckee