Archived

This topic is now archived and is closed to further replies.

virtual assignment operators

This topic is 5085 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

Recommended Posts

ok C++ oo guru types... virtual assignment operators: good or bad? Personnally I would never have thought to make an assignment operator virtual...but in my new job there are several such operators. Problem is when the a class is dervied from the base, the compiler complains that the virtual = op is hidden. The derived class obviously needs it''s own assignment op in order to correctly copy the derived data, and can just call the base impl. Am I missing something? Any thoughts? "That''s not a bug, it''s a feature!" --me

Share on other sites
My personal opinion is virtual assignment is evil, especially virtual copy assignment. In almost all cases a design that uses virtual assignment can be reimplemented in terms of non-operator member functions. (edit: That is, non-operator virtual functions.)

One of the evils of virtual copy assignment is that it can allow a two classes derived to the same base to be assigned to each other, which rarely makes sense.

Another issue with virtual copy assignment is that proper implementation of a virtual copy assignment operator requires an override for each and every assignment operator defined by the base classes of a derived class, which often leads to an geometric growth of assignment operator implementations.

One final evilness of virtual copy assignment is that it promotes unnecessary coupling between classes.

And this is just off the top of my head. I could probably rant on for a while longer if I did some research.

[edited by - SiCrane on March 18, 2004 9:05:48 PM]

Share on other sites
My first reaction was that a virtual assignment operator is not useful. Then I did some quick tests and found that a virtual assignment operator is useful for overcoming a problem with slicing. I don''t know if that is the best way to solve the problem. The slicing problem can be handled because the derived class has a chance to synchronize its state with the base class. I don''t think it is evil because it doesn''t create any extra potential for problems or abuse, even though it is unusual.
quote:
Original post by SiCrane
...it can allow a two classes derived to the same base to be assigned to each other...
Not true. The assignment is possible with or without overloading the assignment operator, virtual or non-virtual.
quote:
Original post by SiCrane
...proper implementation of a virtual copy assignment operator requires an override for each and every assignment operator...
Only if the base class function is pure. Otherwise, it is just like any other virtual function.

Share on other sites
quote:
Original post by JohnBolton
My first reaction was that a virtual assignment operator is not useful. Then I did some quick tests and found that a virtual assignment operator is useful for overcoming a problem with slicing.

In general, virtual copy assignment doesn't solve slicing. Assigning a derived class to a base class will not magically give the base class the derived class's member variables or update the base class's vtable.

quote:
quote:
Original post by SiCrane
...it can allow a two classes derived to the same base to be assigned to each other...

Not true. The assignment is possible with or without overloading the assignment operator, virtual or non-virtual.

Be consistent. First you say that it's not possible then you say that it is possible. I'll assume that you mean that it is possible, because that's the longer of the two sentences you typed. In which case you would be wrong. Assignment between derived classes cannot be performed unless the derived classes specifically define assignment operators for the base class or the other derived class. This is due to function hiding and automatic assigment operator generation. For example, this code will not compile on a standards compliant compiler:
class Base {  public:    Base & operator=(const Base &) { return *this; }};class Derived1 : public Base {};class Derived2 : public Base {};int main(int, char **) {  Derived1 d1;  Derived2 d2;  d1 = d2;  return 0;}
quote:
quote:
Original post by SiCrane
...proper implementation of a virtual copy assignment operator requires an override for each and every assignment operator...
Only if the base class function is pure. Otherwise, it is just like any other virtual function.

Wrong again. Assignment operators are one of the small number of functions that that compiler generates implicitly for each class. And the assignment operator generated by the compiler is a non-virtual function that will hide the virtual function in the base class. This makes it unlike other virtual functions.

edit: formatting

[edited by - SiCrane on March 19, 2004 2:28:18 AM]

Share on other sites
quote:
Original post by SiCrane
quote:
Original post by JohnBolton
...found that a virtual assignment operator is useful for overcoming a problem with slicing.

In general, virtual copy assignment doesn''t solve slicing...

The code I tried was something like this:
class A  { .... };class B : public A { .... };class C : public A { .... };...A   a;A * pb = new B;A * pc = new C;*pb = *pc;*pc = a;
Overriding the A''s virtual assignment operator in B with
B const & B::operator=( A const & a ) {...}
allows B to deal with any slicing issues that may occur. I tried it. It worked.
quote:
Original post by SiCrane
quote:
Original post by JohnBolton
quote:
Original post by SiCrane
...it can allow a two classes derived to the same base to be assigned to each other...
Not true...
...Assignment between derived classes cannot be performed unless the derived classes specifically define assignment operators for the base class or the other derived class...
Well, the code above compiles and runs without implementing any assignment operators. It also compiles and runs if only an assignment operator for A is provided, virtual or non-virtual. That is what I meant.
quote:
Original post by SiCrane
quote:
Original post by JohnBolton
quote:
Original post by SiCrane
...proper implementation of a virtual copy assignment operator requires an override for each and every assignment operator...
Only if the base class function is pure. Otherwise, it is just like any other virtual function.
Wrong again.
Well, I based my statement on the things I tried. If nothing was overridden, the assignment copied the member variables of A. If I overrode A''s assignment operator in A (virtual or not) but not B, the overriding function in A was called. If I overrode A''s assignment operator in A (virtual) and in B, then B''s function was called. I don''t see any difference between this and how other virtual functions work.

Share on other sites
quote:
Original post by JohnBolton
I tried it. It worked.

It may have invoked the proper virtual function, but did it reassign virtual function table pointers or magically grow member variables defined in the C class in the B object? I don''t think you understand the definition of slicing.

quote:

Well, the code above compiles and runs without implementing any assignment operators. It also compiles and runs if only an assignment operator for A is provided, virtual or non-virtual. That is what I meant.

And it will fail when working with the concrete types rather than the abstract types. When additional type information causes an operation to fail when limited type information allows the operation to succeed, that''s what we call a warning sign. C++ is a strongly typed language for a reason. Attempting to subvert the type system is deliberately asking for trouble.

quote:
Well, I based my statement on the things I tried. If nothing was overridden, the assignment copied the member variables of A. If I overrode A''s assignment operator in A (virtual or not) but not B, the overriding function in A was called. If I overrode A''s assignment operator in A (virtual) and in B, then B''s function was called. I don''t see any difference between this and how other virtual functions work.

The difference was you were working with only the abstract types. The behavior of the concrete types is another story. And if there''s a disjunction between the behaviour of an object when accessed via an abstract type versus its behaviour when accessed with its concrete type, then we have bad mojo (or "evilness").

In contrast, if we defined a normal virtual function, say twiddle() in the base class, then this schizophrenic behaviour does not occur with respect to twiddle() in the derived classes. (Unless you go out of your way to induce schizophrenia.) This is why a virtual assignment operator is unlike a normal virtual function.

Share on other sites
quote:
Original post by JohnBolton
The code I tried was something like this:
class A  { .... };class B : public A { .... };class C : public A { .... };...A   a;A * pb = new B;A * pc = new C;*pb = *pc;*pc = a;
Overriding the A''s virtual assignment operator in B with
B const & B::operator=( A const & a ) {...}
allows B to deal with any slicing issues that may occur. I tried it. It worked.

I dont see what you have gained here, you still only have access to the A member variables of the a parameter in B::operator=. To avoid ''slicing'', as you put it, you would need to implement some form of double dispatch here.

Share on other sites
quote:
Original post by Jingo
To avoid ''slicing'', as you put it, you would need to implement some form of double dispatch here.

Double dispatch won''t help with slicing. The fundamental problem is that the concrete type of a given lvalue is immutable under standard C++ rules. Neither double dispatch or virtual assignment will change that.

Share on other sites
Why wont double dispatch help?

A* pb = new b;A* pb2 = new b;assign(pb, pb2); //calls b::operator=(b&) using double dispatch

Problem solved, the correct operator = would have access to all members of the dynamic type.

Share on other sites
The problem is when you assign a B object to an A object or a C object. Neither assignment generally makes sense, and both assignments are syntactically possible with a double dispatch approach. In both cases there are most likely data members that exist in the assigned type don''t exist in the assignee type. This leads to classic slicing.

Share on other sites
I can't see how it could possibly be the correct thing to do.

Jingo: But that's doesn't require anything special. Virtual assignment means you want to do something crazy like this:

Base* b1 = new A;Base* b2 = new B;*b1 = *b2;

If it can work, then it could be implemented in the base class.
In all cases, it is a fragile design as it is begging to slice objects.

The "right" way is to use a co-variant return type with a virtual clone method.
Base* b1 = new A;Base* b2 = 0;b2 = b1.Clone();

[edited by - Magmai Kai Holmlor on March 21, 2004 3:37:13 PM]

[edited by - Magmai Kai Holmlor on March 21, 2004 3:38:16 PM]

Share on other sites
*cough* b1->Clone();

Share on other sites
quote:
Original post by SiCrane
The problem is when you assign a B object to an A object or a C object. Neither assignment generally makes sense, and both assignments are syntactically possible with a double dispatch approach. In both cases there are most likely data members that exist in the assigned type don''t exist in the assignee type. This leads to classic slicing.

Depends on the approach you take. A simple brute force method could prevent anything other than a B being assigned to a B, though it does require knowledge of types derived from A.

void assign(A* p1, A* p2){  if(dynamic_cast<B*>(p1) && !dynamic_cast<C*>(p1))  {     if(dynamic_cast<B*>(p2) && !dynamic_cast<C*>(p2))     {         //call whatever     }   }//throw exception here, or some sort of assert}

This would only allow B to be assigned to B, and prevent the slicing.

Share on other sites
And what have you actually gained by doing all this? You may have "solved" slicing, but your code does not actually address the real issue of creating a copy of an object of the asigned type in the general case. Furthermore it now exhibits an extremely fragile dependence on the exact structure of the object hierarchy, making it a maintenence nightmare.

Additionally, an assertion or an exception in the double dispatching code both imply that the exact concrete types of both arguments are already known to be equal as a pre-condition to the function call. If your code maintains sufficient type information so that this is known to be the case then the whole dynamic-dispatch mechanism is unnecessary. If the code does not maintain sufficient type information so that this known to the be the case, then an exception would have to be handled properly. And what kind of exception handling is appropriate in this kind of situation? Assignment of incompatible types is a very fundamental logic error.

What this all boils down to is that in C++ it''s a big problem to try to use polymporphic objects as value types.

Share on other sites
right, if C++ (my favorite lanauge), had attributes (aka C#), I would definately desire something akin to:

[NoPolyMorphicCopy]
class MyClass
{
};

so that on a per class (or at least per base class) basis, you could tell the compiler when such copies are valid or invalid.

Obviously the rules of C++ say such things are valid ... but in practice they usually aren''t:

for instance it is NOT valid to take a SuperSecurityProtocol - and VALUE copy it to a base SecurityProtocol object .. you loose it''s operation ... as is expect, but you likely also violate class variants ...

what we can see from this case is (or seems to me), that this is where the whole important distinction of INTERFACES vs. base CLASSES comes in ... IF you''r object''s cannot be value copied into their base-classes correctly, a slightly better approach would have been to use an INTERFACE shared by both and then containment to get the implementation ...

but of course, then we have to write all those damn wrapper stubs to wire it up ... like all the other stupid language out their with no multipl inheiritance ... so this is not ideal either.

and you cannot use protected or private inheiritance comibined with interface inheiritance ... cause then you have all sorts of hidden / ambiguity issues ...

I think we''re finally hitting a case where ... the C++ langauge default is only reasonable in half the cases ... and it isn''t this half ... and their is really nothing you can do about it except educate your programmers to know basically what SiCrane said ... which is:

"What this all boils down to is that in C++ it''s a big problem to try to use polymporphic objects as value types."

Share on other sites
quote:
Original post by SiCrane
And what have you actually gained by doing all this? You may have "solved" slicing, but your code does not actually address the real issue of creating a copy of an object of the asigned type in the general case.

I think the code provides a concept which can be used in the general case.

quote:
Furthermore it now exhibits an extremely fragile dependence on the exact structure of the object hierarchy, making it a maintenence nightmare.

Such is the nature of the brute force approach. Though im sure a loki style approach to the brute force dispatcher could make code maintenence less of an issue.

quote:

Additionally, an assertion or an exception in the double dispatching code both imply that the exact concrete types of both arguments are already known to be equal as a pre-condition to the function call.

I disagree, the assertion implies that it is a logic error for the dynamic types of the two arguments to be anything other than equal. The exact types of the two arguments do not need to be known, it doesn''t matter whether they are of type b, type c, or type a, they just have to both be equal, or not, depending on what you are trying to achieve. This approach would work independent of what the type is, as long as all types derived from a are known.

quote:
And what kind of exception handling is appropriate in this kind of situation? Assignment of incompatible types is a very fundamental logic error.

logic_error should do it then

Share on other sites
quote:
Original post by Jingo
I think the code provides a concept which can be used in the general case.

Your code does not generalize. For one, what you posted is a specialization of the general double dispatch idiom in order to prevent slicing. Generalizing it would bring us back to the original set of objections.

Also, the entire premise of your double dispatch approach is still based on the concept of assignment. The type of an object in C++, once created, is immutable. Therefore, the general case, polymorphically creating a copy of an object of a given type cannot be solved by assignment under normal circumstances.

quote:

I disagree, the assertion implies that it is a logic error for the dynamic types of the two arguments to be anything other than equal.

And if you know that much, then the full double dispatch mechanism is unnecessary. If you take equality of concrete types to be a precondition then the mechanism could be rewritten as a virtual function implemented in each class in the hierachy that takes a pointer or reference to the base class. Then a simple assertion on type ids could be performed, followed by a static cast. But wait! That sounds suspiciously like a virtual assignment operator!

Double dispatch does not solve the fundamental issues behind virtual assignment, it merely covers it up with smoke and mirrors. Which brings me back to my original question: And what have you gained by doing all of this?

Share on other sites
quote:
Original post by SiCrane
And if you know that much, then the full double dispatch mechanism is unnecessary

You do not know that they are equal, its just an error for them to be anything other than equal.

quote:

Which brings me back to my original question: And what have you gained by doing all of this?

A method of assignment of dynamic types without slicing which provides error signaling when illogical assignments are attempted.

Share on other sites
Re-read my post. You can get both of those without using the double-dispatch mechanism. So what have you gained by obfuscating your code with double dispatch?