虚赋值运算符 C++

发布于 2024-07-15 17:55:13 字数 53 浏览 5 评论 0原文

C++ 中的赋值运算符可以变为虚拟的。 为什么需要它? 我们可以让其他运营商也虚拟化吗?

Assignment Operator in C++ can be made virtual. Why is it required? Can we make other operators virtual too?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

止于盛夏 2024-07-22 17:55:13

赋值运算符不需要是虚拟的。

下面的讨论是关于 operator= 的,但它也适用于接受相关类型的任何运算符重载,以及接受相关类型的任何函数。

下面的讨论表明,virtual 关键字在查找匹配的函数签名时不知道参数的继承。 在最后一个示例中,它展示了在处理继承类型时如何正确处理赋值。


虚拟函数不知道参数的继承:

函数的签名必须与虚拟函数相同才能发挥作用。 因此,即使在下面的示例中,operator=被设为虚函数,该调用也永远不会充当D中的虚函数,因为operator=的参数和返回值是不同的。

函数 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 变量实际上是将 D 存储到 B 的引用中。您将不会获得 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) 上没有 virtual 关键字,您将得到与上面相同的结果,但 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;
}

The assignment operator is not required to be made virtual.

The discussion below is about operator=, but it also applies to any operator overloading that takes in the type in question, and any function that takes in the type in question.

The below discussion shows that the virtual keyword does not know about a parameter's inheritance in regards to finding a matching function signature. In the final example it shows how to properly handle assignment when dealing with inherited types.


Virtual functions don't know about parameter's inheritance:

A function's signature needs to be the same for virtual to come into play. So even though in the following example, operator= is made virtual, the call will never act as a virtual function in D, because the parameters and return value of operator= are different.

The function B::operator=(const B& right) and D::operator=(const D& right) are 100% completely different and seen as 2 distinct functions.

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

Default values and having 2 overloaded operators:

You can though define a virtual function to allow you to set default values for D when it is assigned to variable of type B. This is even if your B variable is really a D stored into a reference of a B. You will not get the D::operator=(const D& right) function.

In the below case, an assignment from 2 D objects stored inside 2 B references... the D::operator=(const B& right) override is used.

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

Prints:

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

Which shows that D::operator=(const D& right) is never used.

Without the virtual keyword on B::operator=(const B& right) you would have the same results as above but the value of y would not be initialized. I.e. it would use the B::operator=(const B& right)


One last step to tie it all together, RTTI:

You can use RTTI to properly handle virtual functions that take in your type. Here is the last piece of the puzzle to figure out how to properly handle assignment when dealing with possibly inherited types.

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;
}
薄情伤 2024-07-22 17:55:13

这取决于运营商。

将赋值运算符设为虚拟的目的是让您能够覆盖它以复制更多字段。

因此,如果您有一个 Base& 你实际上有一个 Derived& 作为动态类型,并且 Derived 有更多字段,会复制正确的内容。

但是,存在这样的风险:您的 LHS 是 Derived,而 RHS 是 Base,因此当虚拟运算符在 Derived 中运行时,您的参数不是 Derived,并且您无法从中获取字段。

这是一个很好的讨论:
http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html

It depends on the operator.

The point of making an assignment operator virtual is to allow you from the benefit of being able to override it to copy more fields.

So if you have an Base& and you actually have a Derived& as a dynamic type, and the Derived has more fields, the correct things are copied.

However, there is then a risk that your LHS is a Derived, and the RHS is a Base, so when the virtual operator runs in Derived your parameter is not a Derived and you have no way of getting fields out of it.

Here is a good discussio:
http://icu-project.org/docs/papers/cpp_report/the_assignment_operator_revisited.html

凶凌 2024-07-22 17:55:13

Brian R. Bondy 写道:

<小时>

将所有内容结合在一起的最后一步,RTTI:

您可以使用 RTTI 来正确处理采用您的类型的虚拟函数。 这是解决在处理可能继承的类型时如何正确处理赋值的最后一块难题。

虚拟 B&   运算符=(const B&右) 
  { 
    const D *pD =dynamic_cast(&right); 
    如果(pD) 
    { 
      x=pD->x; 
      y=pD->y; 
    } 
    别的 
    { 
      x = 右.x; 
      y = 13;//默认值 
    } 

    返回*这个; 
  } 
  

我想对此解决方案添加一些评论。 将赋值运算符声明为与上面相同会带来三个问题。

编译器生成一个赋值运算符,它接受一个 const D& 参数,该参数不是虚拟的,并且不会执行您可能认为它会执行的操作。

第二个问题是返回类型,您将返回对派生实例的基引用。 可能问题不大,因为代码无论如何都能工作。 不过,最好还是相应地返回参考文献。

第三个问题,派生类型赋值运算符不会调用基类赋值运算符(如果您想要复制私有字段怎么办?),将赋值运算符声明为 virtual 不会使编译器为您生成赋值运算符。 这实际上是没有至少两次重载赋值运算符来获得所需结果的副作用。

考虑基类(与我引用的帖子中的相同):

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 D& 的运算符采用const D2&。 结论很明显,operator =()重载的数量相当于超类的数量+1。

考虑到D2继承了D,我们来看看继承的两个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;
};

不需要设置默认值方法,因为它只会接收一次调用(在基本运算符 =() 重载中)。 复制字段时的更改在一处完成,并且所有运算符 =() 重载都会受到影响并实现其预期目的。

感谢 sehe 的建议。

Brian R. Bondy wrote:


One last step to tie it all together, RTTI:

You can use RTTI to properly handle virtual functions that take in your type. Here is the last piece of the puzzle to figure out how to properly handle assignment when dealing with possibly inherited types.

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

I would like to add to this solution a few remarks. Having the assignment operator declared the same as above has three issues.

The compiler generates an assignment operator that takes a const D& argument which is not virtual and does not do what you may think it does.

Second issue is the return type, you are returning a base reference to a derived instance. Probably not much of an issue as the code works anyway. Still it is better to return references accordingly.

Third issue, derived type assignment operator does not call base class assignment operator (what if there are private fields that you would like to copy?), declaring the assignment operator as virtual will not make the compiler generate one for you. This is rather a side effect of not having at least two overloads of the assignment operator to get the wanted result.

Considering the base class (same as the one from the post I quoted):

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

    int x;
};

The following code completes the RTTI solution that I quoted:

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

This may seem a complete solution, it's not. This is not a complete solution because when you derive from D you will need 1 operator = that takes const B&, 1 operator = that takes const D& and one operator that takes const D2&. The conclusion is obvious, the number of operator =() overloads is equivalent with the number of super classes + 1.

Considering that D2 inherits D, let's take a look at how the two inherited operator =() methods look like.

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

It is obvious that the operator =(const D2&) just copies fields, imagine as if it was there. We can notice a pattern in the inherited operator =() overloads. Sadly we cannot define virtual template methods that will take care of this pattern, we need to copy and paste multiple times the same code in order to get a full polymorphic assignment operator, the only solution I see. Also applies to other binary operators.


Edit

As mentioned in the comments, the least that can be done to make life easier is to define the top-most superclass assignment operator =(), and call it from all other superclass operator =() methods. Also when copying fields a _copy method can be defined.

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

There is no need for a set defaults method because it would receive only one call (in the base operator =() overload). Changes when copying fields are done in one place and all operator =() overloads are affected and carry their intended purpose.

Thanks sehe for the suggestion.

烧了回忆取暖 2024-07-22 17:55:13

虚拟分配用于以下场景:

//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) 虚拟概念

现在只需使用virtual Base&即可。 operator=(const Base& obj) 不会有帮助,因为 ChildBaseoperator= 的签名不同。

我们需要添加Base& Child 类中的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;

在这种情况下,编译器在 Child 中查找 operator=(Base& obj) 定义作为 operator = 在 Child 上调用。 但由于它不存在,并且 Base 类型无法隐式提升为 child,因此会出现错误。(需要进行强制转换,如 obj1=dynamic_cast(*ptr1);)

如果我们按照case2&3实现,这种情况就会得到处理。

可以看出,在使用基类指针/引用进行赋值的情况下,虚拟赋值使调用更加优雅。

我们可以让其他运营商也虚拟化吗?

virtual assignment is used in below scenarios:

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

case 1: obj1 = obj2;

In this virtual concept doesn't play any role as we call operator= on Child class.

case 2&3: *ptr1 = obj2;

              
   *ptr1 = *ptr2;

Here assignment won't be as expected. Reason being operator= is called on Base class instead.

It can be rectified using either:
1) Casting

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

2) Virtual concept

Now by simply using virtual Base& operator=(const Base& obj) won't help as signatures are different in Child and Base for operator=.

We need to add Base& operator=(const Base& obj) in Child class along with its usual Child& operator=(const Child& obj) definition. Its important to include later definition, as in the absence of that default assignment operator will be called.(obj1=obj2 might not give desired result)

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

case 4: obj1 = *ptr2;

In this case compiler looks for operator=(Base& obj) definition in Child as operator= is called on Child. But since its not present and Base type can't be promoted to child implicitly, it will through error.(casting is required like obj1=dynamic_cast<Child&>(*ptr1);)

If we implement according to case2&3, this scenario will be taken care of.

As it can be seen virtual assignment makes call more elegant in case of assignments using Base class pointers/reference .

Can we make other operators virtual too? Yes

网白 2024-07-22 17:55:13

仅当您想要保证从您的类派生的类正确复制其所有成员时才需要它。 如果您没有使用多态性做任何事情,那么您实际上不需要担心这一点。

我不知道有什么会阻止您虚拟化您想要的任何运算符——它们只不过是特殊情况的方法调用。

此页面提供了关于这一切如何进行的出色而详细的描述作品。

It's required only when you want to guarantee that classes derived from your class get all of their members copied correctly. If you aren't doing anything with polymorphism, then you don't really need to worry about this.

I don't know of anything that would prevent you from virtualizing any operator that you want--they're nothing but special case method calls.

This page provides an excellent and detailed description of how all this works.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文