74

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

4

5 に答える 5

58

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

以下の説明は に関するものです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 つのオーバーロードされた演算子を持つ:

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

以下のケースでは、2 つの B 参照内に格納された 2D オブジェクトからの割り当て...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)決して使用されていないことを示しています。

virtual キーワードが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;
}
于 2009-03-21T20:05:59.430 に答える
25

オペレーターによって異なります。

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

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

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

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

于 2009-03-21T19:34:45.653 に答える
8

ブライアン・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 から派生する場合、const B&を取る operator = 1 つ、 const D&を取る operator = 1 つ、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;
    }
};

演算子 =(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 か所で行われ、すべての演算子 =() オーバーロードが影響を受け、意図した目的を実行する場合の変更。

提案してくれてありがとう。

于 2012-09-04T14:07:30.490 に答える
6

仮想割り当ては、次のシナリオで使用されます。

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

operator=この仮想概念では、私たちがChildクラスを呼び出すとき、何の役割も果たしません。

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

ここで、割り当ては期待どおりになりません。Reason beingは代わりにクラスoperator=で呼び出されます。Base

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

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

2) バーチャルコンセプト

とでは署名が異なるため、単純に使用してvirtual Base& operator=(const Base& obj)も役に立ちません。ChildBaseoperator=

通常の定義Base& operator=(const Base& obj)とともに Child クラスを追加する必要があります。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=(Base& obj)定義を探します。しかし、存在せず、型を暗黙的に昇格できないため、エラーが発生します(のようにキャストが必要です) 。Childoperator=Basechildobj1=dynamic_cast<Child&>(*ptr1);

ケース 2 と 3 に従って実装すると、このシナリオは処理されます。

ご覧のとおり、基本クラスのポインター/参照を使用した割り当ての場合、仮想割り当てにより呼び出しがよりエレガントになります。

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

于 2014-11-13T10:11:02.327 に答える
4

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

必要な演算子を仮想化するのを妨げるものは何も知りません。それらは、特殊なケースのメソッド呼び出しに他なりません。

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

于 2009-03-21T19:31:17.970 に答える