Virtual Assignment Operator in Protocol class

N

N4M

Dear,
Suppose I have a Protocol class, in which I need also an assignment
operator =().
class B
{
.....
virtual B& operator=(const B& rb) =0;
};
Now in some derived class D: public B, how would I proceed with
operator=? Do I need to supply 2 operators:
-01 to override D& operator=(const B& d)
-and 01 to overload D& operator=(const D& d)?
How about the (im)purality of = ?
Thanks for your guidance.
 
K

Karl Heinz Buchegger

N4M said:
Dear,
Suppose I have a Protocol class, in which I need also an assignment
operator =().
class B
{
....
virtual B& operator=(const B& rb) =0;
};
Now in some derived class D: public B, how would I proceed with
operator=? Do I need to supply 2 operators:
-01 to override D& operator=(const B& d)
-and 01 to overload D& operator=(const D& d)?
How about the (im)purality of = ?
Thanks for your guidance.

In a nutshell: having op= as virtual is seldome a good idea. Mostly
because it doesn't work how most people would expect it to work.
Hint: polymorphism works only by examining the object the function
is called for, but doesn't take the runtime type of the arguments
into account.

Assume class D, derived from class B

B* p1 = new D;
B* p2 = new D;

*p1 = *p2;

In the above, what do you think the compiler is looking for
when searching a function to fullfil the request of the
assignment? :

1) B::eek:perator=( const B& Arg );
or 2) B::eek:perator=( const D& Arg );
or 3) D::eek:perator=( const B& Arg );
or 4) D::eek:perator=( const D& Arg );
 
K

Karl Heinz Buchegger

Karl said:
In a nutshell: having op= as virtual is seldome a good idea. Mostly
because it doesn't work how most people would expect it to work.
Hint: polymorphism works only by examining the object the function
is called for, but doesn't take the runtime type of the arguments
into account.

After rethinking (and smoking a cigarette) I don't think this to be
relevant any more. Read on ...
Assume class D, derived from class B

B* p1 = new D;
B* p2 = new D;

*p1 = *p2;

In the above, what do you think the compiler is looking for
when searching a function to fullfil the request of the
assignment? :

1) B::eek:perator=( const B& Arg );
or 2) B::eek:perator=( const D& Arg );
or 3) D::eek:perator=( const B& Arg );
or 4) D::eek:perator=( const D& Arg );

Next thought experiment (you might want to try this with your compiler
anyway): make the operator virtual.
Which op= is called then?

Hint: Think about what a dereference operation does when the runtime
type of an object differs from the static type of the pointer pointing
to it.
 
R

Risto Lankinen

Karl Heinz Buchegger said:
In a nutshell: having op= as virtual is seldome a good idea. Mostly
because it doesn't work how most people would expect it to work.
Hint: polymorphism works only by examining the object the function
is called for, but doesn't take the runtime type of the arguments
into account.

Then programmer must do that, no?
Assume class D, derived from class B

B* p1 = new D;
B* p2 = new D;

*p1 = *p2;

In the above, what do you think the compiler is looking for
when searching a function to fullfil the request of the
assignment? :

1) B::eek:perator=( const B& Arg );
or 2) B::eek:perator=( const D& Arg );
or 3) D::eek:perator=( const B& Arg );
or 4) D::eek:perator=( const D& Arg );

The symptoms you demonstrate do not exist (nor become better
or worse) because of virtual assignment, but because of how
C++ object model works. In real world, virtual assignment is not
the disease, but the cure!

Consider this example:

// ========================================

struct B
{
// this is a pretty canonical class, except for virtual assignment

virtual B &operator=( const B & );

// virtual destructor needed for derived deletion thru base *

virtual ~B();

// add more members (data or functions) to suit the taste...
};


struct D : public B
{
int value;

D( int i=0 ) : value(i)
{
}

D( const D &r ) : B(r) , value(r.value)
{
}

virtual D &operator=( const D &r )
{
B::eek:perator=( r );
value = r.value;
return *this;
}

virtual B &operator=( const B &r )
{
const D *p = dynamic_cast<const D *>(&r);

if( p )
{
*this = *p;
}
else
{
// D is being sliced, but now you can detect and compensate!!!
B::eek:perator=( r );
value = 0;
}

return *this;
}
};

// ========================================

Now your example...
B* p1 = new D;
B* p2 = new D;

*p1 = *p2;

.... will assign as-if using the most intuitive choice...
1) B::eek:perator=( const B& Arg );
or 2) B::eek:perator=( const D& Arg );
or 3) D::eek:perator=( const B& Arg );
or 4) D::eek:perator=( const D& Arg );

.... which, of course, is 4 .

- Risto -
 
K

Karl Heinz Buchegger

Risto said:
... which, of course, is 4 .

- Risto -

Thanks for bringing me back on track.
I don't know why, but somehow I always get lost when
thinking about a virtual op= :)
 
N

N4M

Thanks for your example, but what if it is insisted to have all
operators and functions in the ABC be PURE ? Then I cannot initiate B
b...
That's driving my crazy.
N4M
 
N

N4M

Thanks for your example, but what if it is insisted to have all
operators and functions in the ABC be PURE ? Then I cannot initiate B
b...
That's driving my crazy.
N4M
 
K

Karl Heinz Buchegger

N4M said:
Thanks for your example, but what if it is insisted to have all
operators and functions in the ABC be PURE ? Then I cannot initiate B
b...
That's driving my crazy.

I don't understand.

Making those functions PURE (by adding = 0), doesn't mean that
you cannot provide an implementation for them :)
 
D

Daniel T.

Dear,
Suppose I have a Protocol class, in which I need also an assignment
operator =().
class B
{
....
virtual B& operator=(const B& rb) =0;
};
Now in some derived class D: public B, how would I proceed with
operator=?

Let's consider this tree:

class B {
public:
virtual ~B() { }
virtual B& operator=( const B& ) = 0;
};

class D {
int foo;
public:
virtual B& operator=( const B& );
};

class C {
double bar;
public:
virtual B& operator=( const B& );
};

Can either D::eek:p= or C::eek:p= be implemented reasonably? I don't think so.
What is supposed to happen with this code?

void fun( B& b1, B& b2 ) {
b1 = b2;
}

If b1 is an D and b2 is a C?

Do I need to supply 2 operators:
-01 to override D& operator=(const B& d)

You need to provide this one.
-and 01 to overload D& operator=(const D& d)?

You would only need this one if something in the class requires it,
otherwise it will be created properly for you.

The moral of the story here is "don't provide a pure virtual op=".
 
R

Risto Lankinen

Daniel T. said:
Let's consider this tree:

class B {
public:
virtual ~B() { }
virtual B& operator=( const B& ) = 0;
};

class D {
int foo;
public:
virtual B& operator=( const B& );
};

class C {
double bar;
public:
virtual B& operator=( const B& );
};

Can either D::eek:p= or C::eek:p= be implemented reasonably?

Absolutely. Here's one way:

B &D::eek:perator=( const B &r )
{
const D *p = dynamic_cast<const D *>(&r);
if( p )
foo = p->foo;
else
throw "Silly assignment!";
return *this;
}

B &C::eek:perator=( const B &r )
{
const C *p = dynamic_cast<const C *>(&r);
if( p )
bar = p->bar;
else
throw "Silly assignment!";
return *this;
}
What is supposed to happen with this code?

void fun( B& b1, B& b2 ) {
b1 = b2;
}

If b1 is an D and b2 is a C?

With virtual assignment, it would throw a "Silly assignment!"
exception (or alternatively, whatever else the author of the
class D deems appropriate for the case when, so to speak,
an Orange is assigned to a Banana thru a reference to Fruit).

WithOUT virtual assignment it would create a banana the size
of a grape, with an orange skin that doesn't need to be peeled
before eating.
The moral of the story here is "don't provide a pure virtual op=".

This belief, I think, needs reconsideration.

- Risto -
 
D

Daniel T.

Risto Lankinen said:
Absolutely. Here's one way:

B &D::eek:perator=( const B &r )
{
const D *p = dynamic_cast<const D *>(&r);
if( p )
foo = p->foo;
else
throw "Silly assignment!";
return *this;
}

B &C::eek:perator=( const B &r )
{
const C *p = dynamic_cast<const C *>(&r);
if( p )
bar = p->bar;
else
throw "Silly assignment!";
return *this;
}

I wouldn't call that a reasonable implementation, but maybe that's just
me. If the "Silly assignment" is thrown, the calling code can't do
anything resonable with it and in order for the calling code to avoid
the throw, it must use RTTI to ensure both arguments are the same
derived type, then call op= which again uses dynamic_cast. That sounds
quite expensive...
 
N

N4M

This belief, I think, needs reconsideration.
- Risto -

How do you think about this solution:

class D; //forward
class B
{
public:
virtual ~B() = 0;
virtual B& operator=(const B&b) = 0;
virtual void Assign(D&) const = 0;
};
B::~B() {}
class D: public B
{
public:
explicit D(int i):m_n(i) {}
~D() {}
B& operator=(const B& rb) {
rb.Assign(*this);
return dynamic_cast<B&>(*this);//static_cast is fine, too.
}
virtual void Assign(D& rd) const { rd.m_n = m_n;}
int GetValue() {return m_n;}
private:
int m_n;

};
// Test
void Test(B& b1, B& b2)
{
b1 = b2;
}
//
int main()
{
D d1(1), d2(2);
cout<<"Before d1, d2:"<< d1.GetValue() <<","<<d2.GetValue()<<endl;
Test(d1,d2);
cout<<"After d1, d2:"<< d1.GetValue() <<","<<d2.GetValue()<<endl;
}
I have tested this, it's fine, and it serves the purpose that all
Derived classes need to define the assignment operator indirectly thru
Assign().
Nguyen Mai.
 
R

Risto Lankinen

Daniel T. said:
If the "Silly assignment" is thrown, the calling code can't do
anything resonable with it...

First, I'll repaste a section deleted from my original article:

If you deem the default behaviour (by which I mean simulating
the non-virtual assignment) more appropriate, you could do the
following:

B &D::eek:perator=( const B &r )
{
const D *p = dynamic_cast<const D *>(&r);'
if( p )
// Calls D::eek:perator=( const D & ); Explicit call deliberately
// not used to enable late binding for D::eek:p=(D) in case it has
// been declared virtual in this class and similarly overloaded
// in the next level derived class:
return *this = *p;
else
return B::eek:perator=( p );
}
... and in order for the calling code to avoid the throw it must
use RTTI to ensure both arguments are the same derived type,
then call op= which again uses dynamic_cast. That sounds
quite expensive...

If RTTI is too costly, simply leave it away! But then you can't
completely emulate Derived-to-Derived assignment thru Base
pointers or references. This will still be useful for detecting the
slicing (e.g. to retain the class-level invariants of D consistent
even if someone assigns only a Base part to it):

B &D::eek:perator=( const B &r )
{
B &result = B::eek:perator=( r );
// This 'D' was just sliced by a 'B' called 'r'! Counteract!
return result;
}

Without virtual assignment in base class, this would be impossible.

Cheers!

- Risto -
 
D

Daniel T.

This belief, I think, needs reconsideration.

- Risto -

How do you think about this solution:

class D; //forward
class B
{
public:
virtual ~B() = 0;
virtual B& operator=(const B&b) = 0;
virtual void Assign(D&) const = 0;
};
B::~B() {}
class D: public B
{
public:
explicit D(int i):m_n(i) {}
~D() {}
B& operator=(const B& rb) {
rb.Assign(*this);
return dynamic_cast<B&>(*this);//static_cast is fine, too.
}
virtual void Assign(D& rd) const { rd.m_n = m_n;}
int GetValue() {return m_n;}
private:
int m_n;

};
// Test
void Test(B& b1, B& b2)
{
b1 = b2;
}
//
int main()
{
D d1(1), d2(2);
cout<<"Before d1, d2:"<< d1.GetValue() <<","<<d2.GetValue()<<endl;
Test(d1,d2);
cout<<"After d1, d2:"<< d1.GetValue() <<","<<d2.GetValue()<<endl;
}
I have tested this, it's fine, and it serves the purpose that all
Derived classes need to define the assignment operator indirectly thru
Assign().[/QUOTE]

You end up needing to add an assign to the base class and implement a
new assign method in every other derived class whenever you to create a
derived class. Very ugly...
 
D

Daniel T.

Dear,
Suppose I have a Protocol class, in which I need also an assignment
operator =().
class B
{
....
virtual B& operator=(const B& rb) =0;
};
Now in some derived class D: public B, how would I proceed with
operator=? Do I need to supply 2 operators:
-01 to override D& operator=(const B& d)
-and 01 to overload D& operator=(const D& d)?
How about the (im)purality of = ?
Thanks for your guidance.

I've been thinking more about this whole situation... Given the above
class (ie op= is pure virtual and the class has no data members) what is
the post-condition of the op=? I mean, usually the post-condition of op=
is that *this == rb after the function, but would:

bool fun( B& r, B& l ) {
return r == l;
}

even compile? If yes, under what conditions should the above return
false? Under what condtions should it return true?
 
D

Daniel T.

Risto Lankinen said:
Consider this example:
[formatting changed some to save space. I also added a pure virtual
to the base to make it an ABC like the origional example.]
// ========================================

struct B
{ virtual void foo() = 0;
virtual B &operator=( const B & );
virtual ~B();
};


struct D : public B
{
int value;
D( int i=0 ) : value(i) { }

D( const D &r ) : B(r) , value(r.value) { } void foo() { }

virtual D &operator=( const D &r ) {
B::eek:perator=( r );
value = r.value;
return *this;
}

virtual B &operator=( const B &r ) {
const D *p = dynamic_cast<const D *>(&r);
if( p ) {
*this = *p;
}
else {
// D is being sliced, but now you can detect and compensate!!!
B::eek:perator=( r );
value = 0;
}
return *this;
}
};

Using the example above, given this code:

struct E: public B {
float value;
B(): value( 0.01 ) { }
void foo() { }
B& operator=( const B& r ) {
const E* p = dynamic_cast< const E* >( &r );
if ( p )
*this = *p;
else {
B::eek:perator=( r );
value = 0.01;
}
return *this;
}
};

void ick( B& l, B& r ) {
l = r;
}

int main() {
E e;
D d;

ick( d, e );
}

Are 'd' and 'e' supposed to be equal after the call to 'ick'? Could 'd'
and 'e' ever be equal? If not, then what is B's op= supposed to do?
 
R

Risto Lankinen

Daniel T. said:
Risto Lankinen said:
Consider this example:
[formatting changed some to save space. I also added a pure virtual
to the base to make it an ABC like the origional example.]
// ========================================

struct B
{ virtual void foo() = 0;
virtual B &operator=( const B & );
virtual ~B();
};


struct D : public B
{
int value;
D( int i=0 ) : value(i) { }

D( const D &r ) : B(r) , value(r.value) { } void foo() { }

virtual D &operator=( const D &r ) {
B::eek:perator=( r );
value = r.value;
return *this;
}

virtual B &operator=( const B &r ) {
const D *p = dynamic_cast<const D *>(&r);
if( p ) {
*this = *p;
}
else {
// D is being sliced, but now you can detect and compensate!!!
B::eek:perator=( r );
value = 0;
}
return *this;
}
};

Using the example above, given this code:

struct E: public B {
float value;
B(): value( 0.01 ) { }
void foo() { }
B& operator=( const B& r ) {
const E* p = dynamic_cast< const E* >( &r );
if ( p )
*this = *p;
else {
B::eek:perator=( r );
value = 0.01;
}
return *this;
}
};

void ick( B& l, B& r ) {
l = r;
}

int main() {
E e;
D d;

ick( d, e );
}

Are 'd' and 'e' supposed to be equal after the call to 'ick'?

Well, yes, if you use the same definition of "equal" as
when B &B::eek:p=(B) is non-virtual.
Could 'd' and 'e' ever be equal?

You tell me. Sure you knew that if you implemented
bool operator(B,B) you'd be able compare a C to a D
anyway (again, regardless of whether the assignments
occurred thru virtual or non-virtual operators).

Nevertheless, I don't see it as a big problem that op=()
and op==() lose a bit of consistency in the presence of
type conversions. Even C with no operator overloading
or virtual methods manifests a similar inconsistency:

int i = 12;
double d = 3.4;
i = d;
assert( i == d );


Cheers!

- Risto -
 
D

Daniel T.

Risto Lankinen said:
Well, yes, if you use the same definition of "equal" as
when B &B::eek:p=(B) is non-virtual.

That's a little hard to do when B::eek:p=(B) is pure-virtual...
You tell me. Sure you knew that if you implemented
bool operator==(B,B) you'd be able compare a C to a D
anyway (again, regardless of whether the assignments
occurred thru virtual or non-virtual operators).

Let's do that:

struct Base {
int x;
Base( int a ): x(a) { }
virtual ~Base() { }
};

bool operator==( const Base& l, const Base& r ) {
return l.x == r.x;
}

//====================================================================
struct Derived : Base {
int y;
Derived( int a, int b ): Base( a ), y( b ) { }
};

//====================================================================
void comp( Base& l, Base& r ) {
assert( l == r );
}

int main() {
Derived d1( 0, 1 ), d2( 0, 2 );
comp( d1, d2 );
cout << "OK";
}

(The above prints "OK")
Is this really something anyone would want?
Nevertheless, I don't see it as a big problem that op=()
and op==() lose a bit of consistency in the presence of
type conversions. Even C with no operator overloading
or virtual methods manifests a similar inconsistency:

int i = 12;
double d = 3.4;
i = d;
assert( i == d );

And that causes no end of trouble for those who think that double should
be substitutable for int. It isn't.

By using inheritance (rather than say a conversion function) we are
saying that any type Derived from Base is subsitutable for it.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,482
Members
44,901
Latest member
Noble71S45

Latest Threads

Top