[C++] LSP and Rule of Three

Started by
9 comments, last by Khatharr 10 years, 6 months ago

An interesting question occurred to me today.

In order to observe LSP, if a base class has an explicit copy ctor / assignment operator, do the derived classes all need explicit versions between themselves and the other classes? In other words, if parent Foo has an explicit copy ctor, does Bar need conversion ctors from Foo and Baz? Does Foo need conversion assignment operators for each of its derived classes?

I'm thinking:


Foo* a = new Foo;
Foo* b = new Bar;
Foo* c = new Baz;
*a = *b; //what happens?
void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.
Advertisement

There is no trickery involved.

The assignment operator Foo::operator=() is called, with the dereferenced b object passed as a parameter. The Foo object will do whatever the operator=() code does, by default just going through all the variables on the class and assigning from RHS to LHS. Anything extra in the subclass is ignored.

It is really not much different than if you had called a->SomeFunction(*b); In this case the function is operator=().

Also note that operator=() is automatically hidden from base classes, so Bar and Baz subclasses will need to implement their own if you want to do something special in the assignment operator.

Okay, so in order to uphold LSP, Bar would need explicit definitions for copy/assign from Foo and Baz, and Baz would need them for Foo and Bar?

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

I would say the LSP is a good reason why you shouldn't have public assignment operators for polymorphic classes.

Ah, okay. I was sort of feeling that way, but the book gave an example class that had them and we were supposed to derive from it. I ignored LSP for the assignment (we haven't discussed SOLID yet), but it made me think that there was maybe some sane way of doing this.

void hurrrrrrrr() {__asm sub [ebp+4],5;}

There are ten kinds of people in this world: those who understand binary and those who don't.

Okay, so in order to uphold LSP, Bar would need explicit definitions for copy/assign from Foo and Baz, and Baz would need them for Foo and Bar?

Again, it is subjective. If the object is generally copyable and assignable then it is going to be just fine, no trickery is involved.

The default behavior is to just copy all members from the RHS to LHS. Normally this works just fine, as expected, and does the right thing.

class SomeData { int a; int b; int c; ... };
class MoreData : public SomeData { int x; int y; int z; ... };

SomeData a;
MoreData b;
...
a = b; // sets the value of a.a, a.b, and a.c. Does not know or care about the b object's x, y, or z members because SomeData doesn't have those members.

In this case MoreData is LSP substitutable for SomeData. The operator= says "give me a SomeData class", and the MoreData class is a SomeData class in every respect, so everything works just fine.

If you want to copy the opposite direction, allowing a MoreData object to be assigned from a SomeData object, you are going to need a custom assignment operator because the base class doesn't satisfy LSP 'IS A' of the child class. It is the same for sibling classes, if you want to assign SomeData object to an OtherData object, you will need custom functions for that because they are not LSP substitutable.

If for some reason your object should not be copyable or assignable then when you implement the rule of three --- which is now the rule of five thanks to movable objects in C++11 and the move constructor and move assignment operator --- the non-copyable object should make them private.
The problem with that reasoning is that you're assuming the derived class satisfies LSP and then using that to show assignment is valid rather than looking at assignment as a operation with observable effects as per the definition of LSP. For a class that satisfies the normal value assignment operation requirements if you have Base a, b and after you perform a = b, the a.foo() and b.foo() should have the same results. However, if b is a derived object and foo() is a virtual function, then after a = b, then a.foo() and b.foo() are going to have different results because of slicing. Therefore, public assignment for a polymorphic base class presents an LSP violation in the general case.

The problem with that reasoning is that you're assuming the derived class satisfies LSP and then using that to show assignment is valid rather than looking at assignment as a operation with observable effects as per the definition of LSP. ... Therefore, public assignment for a polymorphic base class presents an LSP violation in the general case.

Yes, which is why I wrote that it is subjective.

Many classes don't make sense for assignment. If assignment doesn't make sense for the object, then the built in assignment should be made private.

Other classes do make sense for assignment. If assignment does make sense, then it should be allowed.

Should a game object be assignable from a subclass? Probably not. Should a row of data from a spreadsheet be assignable from a subclass? Probably. It is up to the programmer to make that distinction.

The original question was: *a = *b; //what happens?

As far as the language is concerned the results are well defined. Whether the well-defined effect is reasonable for the object is up to the programmer, the design and intent, and other external details.

Should a row of data from a spreadsheet be assignable from a subclass? Probably.

It's not clear to me what kind of situation you are talking about here. Do you have a row class and another class that derives from row? Do you have a spreadsheet class and a class that derives from that spreadsheet class? In the former case, I can't think of a situation where a derived row class would make sense. In the latter case, you don't have full object assignment, you're dealing with assignment of member objects, which isn't what the thread is talking about.

This entire post is confusing something very basic - Physical storage and construction concepts that are built in to C/C++ do not (and can not) correctly interact with any polymorphic OO concepts. The entire reason for factory patterns is because you can't go around constructing and copying physical objects and expect any manner of intelligent OO to work. Repeat after me, polymorphic class libraries are NOT COMPATIBLE with conventional C/C++ construction/copy semantics. Instead, you must use virtual functions (aka interfaces you are in control of) to define whatever contracts your LSP classes want to implement, and then do so.

Construction was ALWAYS special in OO. It is IMPOSSIBLE to construct a derived class object of type "Baz" without someone, somewhere knowing how to do so explicitly. So why would you expect a client to be able to invoke non-virtual, non-polymorphic methods that could possible make sense and interact with OO concepts such as LSP.

For this reason, it is often (but not always) best to isolate your designs into 2 types ...

A) Those that are intended to be polymorphic contracts, where the work is destined to be decided by derived classes - in which case abstract base classes / interfaces would prevent this type of misuse completely.

B) Classes that are more traditional / implementation oriented (aka not designed around polymorphism), which would implement their own API usually as non-virtual method. Which doesn't preclude inheriting from them, nor overriding their methods, but does preclude expecting the behavior to be polymorphic.

Since B is a slightly strange concept, I'll give you a real world case I had. I had a Point2D class, which was non-polymorphic. I then had a Point3D class, which inherited from the Point2D class, adding the Z value of course. The point wasn't to polymorphic get 3D behavior from a 2D client, the point was just simple implementation sharing and auto-casting. After using it for a while, I actually decided the auto-casting was honestly a mistake - since truly to "get" a 2D point from a 3D point you should choose which "plane" you want (ie xy, xz or yz, or even some other non-right-angle 2d projection into your 3d space).

As I'm sure most of you are aware, containment, or even non-public inheritance is often a better answer for "B" style situations.

Now if only C++ (and C#) had a syntax to write 1 line to "forward" all of an "interfaces" methods on to a specific contained object, instead of having to write each wrapper function manually. I could rest in peace.

This topic is closed to new replies.

Advertisement