RTTI v.s. Liskov Principle

  • Follow


Hi,

I know that there is a Liskov substitution principle. This principle is
the 
guild line of OOD which says "Functions that use pointers or references
to 
base classes must be able to use objects of derived classes without
knowing 
it". 

But in C++, there is RTTI that violates this principle. 

I would be glad if you can make some comments on how to use RTTI and
Liskov 
substitution principle.

thanks

Jesse  

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply JesseChen 10/16/2003 2:39:36 PM

JesseChen wrote:
> Hi,
>
> I know that there is a Liskov substitution principle. This principle
> is the guild line of OOD which says "Functions that use pointers or
> references to base classes must be able to use objects of derived
> classes without knowing it".
>
> But in C++, there is RTTI that violates this principle.
>
> I would be glad if you can make some comments on how to use RTTI and
> Liskov substitution principle.

Simple.  Read the above again.  it does not say: they have no way to
know
it.  It only says that whatever the base class interface promises, the
derived classes will deliver.

-- 
WW aka Attila



      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply WW 10/16/2003 8:46:54 PM


WW wrote:
 > JesseChen wrote:
 >
 >>Hi,
 >>
 >>I know that there is a Liskov substitution principle. This principle
 >>is the guild line of OOD which says "Functions that use pointers or
 >>references to base classes must be able to use objects of derived
 >>classes without knowing it".
 >>
 >>But in C++, there is RTTI that violates this principle.
 >>
 >>I would be glad if you can make some comments on how to use RTTI and
 >>Liskov substitution principle.
 >
 >
 > Simple.  Read the above again.  it does not say: they have no way to
 > know
 > it.  It only says that whatever the base class interface promises, the
 > derived classes will deliver.

Actually, RTTI *does* violate LSP as it was originally stated: if a
value of type 'B' can be used anywhere a value of type 'A' is expected
without changing the behavior of a program, then 'B' is a subtype of
'A'. The problem is of course that if 'B' doesn't change anything, why
distinguish it from 'A' in the first place?

At this point the discussion turns religious.

-thant


      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Thant 10/17/2003 7:55:07 AM

Thant Tessman wrote:
> WW wrote:
>  > JesseChen wrote:
>  >
>  >>Hi,
>  >>
>  >>I know that there is a Liskov substitution principle. This
>  principle >>is the guild line of OOD which says "Functions that use
>  pointers or >>references to base classes must be able to use objects
>  of derived >>classes without knowing it".
>  >>
>  >>But in C++, there is RTTI that violates this principle.
>  >>
>  >>I would be glad if you can make some comments on how to use RTTI
>  and >>Liskov substitution principle.
>  >
>  >
>  > Simple.  Read the above again.  it does not say: they have no way
>  to > know
>  > it.  It only says that whatever the base class interface promises,
>  the > derived classes will deliver.
>
> Actually, RTTI *does* violate LSP as it was originally stated: if a
> value of type 'B' can be used anywhere a value of type 'A' is expected
> without changing the behavior of a program, then 'B' is a subtype of
> 'A'. The problem is of course that if 'B' doesn't change anything, why
> distinguish it from 'A' in the first place?

I think that is a misunderstanding of the LSP.  And LSP on LSD. ;-)  In C++
you _cannot_ use class B in place of class A.  Only a pointer or a reference
to class A pointing or refering to a class B can be used as a pointer or a
reference to a class A, without knowing it really refers or points to a
class B.

LSP - as I understand it - talks about interface equality.  Call it
interface definitions and observable, guaranteed post conditions.  The RTTI
is not part of the classes interface, it is a language machinery.  And in
C++ the classes interface can only "show up" independly of the class as a
pointer or a reference.

Also IMO LSP has to be "translated" to the language one uses.  In theory it
would be possible to create a language which needs no predefined class
types, every objct carries around its class definition and everything is
bound runtime.  The CA-Clipper solution of OOB was very close to that
"ideal".  In such a language objects will most probably have no sizeof, they
might have RTTI...  but they will certainly be represented by references to
themselves.  Much like in template programming, we have another -
non-inheritance based LSP surfacing there.

> At this point the discussion turns religious.

Not necessarily.  LSP is a high level principle, which has to be
"translated" for the target language.  It can even be translated to generic
programming, which already shows it is pretty generic (no pun intended), not
only OO.  Of coruse, we can make a religious discussion about it. :-)  But
IMO it is rather philosophy.

-- 
WW aka Attila



      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply WW 10/17/2003 11:43:14 AM

Thant Tessman wrote:
> WW wrote:
>  > JesseChen wrote:
>  >
>  >>Hi,
>  >>
>  >>I know that there is a Liskov substitution principle. This principle
>  >>is the guild line of OOD which says "Functions that use pointers or
>  >>references to base classes must be able to use objects of derived
>  >>classes without knowing it".
>  >>
>  >>But in C++, there is RTTI that violates this principle.
>  >>
>  >>I would be glad if you can make some comments on how to use RTTI and
>  >>Liskov substitution principle.
>  >
>  >
>  > Simple.  Read the above again.  it does not say: they have no way to
>  > know
>  > it.  It only says that whatever the base class interface promises, the
>  > derived classes will deliver.
> 
> Actually, RTTI *does* violate LSP as it was originally stated: if a
> value of type 'B' can be used anywhere a value of type 'A' is expected
> without changing the behavior of a program, '

Where did you get the "without changing the behavior of a program"? 
Obviously the point of polymorphism is that the behaviour will change.

> then 'B' is a subtype of
> 'A'. The problem is of course that if 'B' doesn't change anything, why
> distinguish it from 'A' in the first place?

Even granting the other, the point would be that B can have additional 
functionality that is used when treating it as a B, but not used when 
treating it as an A.

Aaron
-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/17/2003 3:03:17 PM

WW wrote:
> Thant Tessman wrote:
> 

[...]

>>Actually, RTTI *does* violate LSP as it was originally stated: if a
>>value of type 'B' can be used anywhere a value of type 'A' is expected
>>without changing the behavior of a program, then 'B' is a subtype of
>>'A'. The problem is of course that if 'B' doesn't change anything, why
>>distinguish it from 'A' in the first place?
> 
> 
> I think that is a misunderstanding of the LSP. [...]

No, it's almost a direct quote. The problem is that as originally 
stated, the LSP is far to restrictive to be useful. And like all 
religious scripture, what one decides what it *really* means determines 
the sect to which one is ordained.

-thant


      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Thant 10/17/2003 3:31:05 PM

Aaron Bentley wrote:
> Thant Tessman wrote:

[...]

>>Actually, RTTI *does* violate LSP as it was originally stated: if a
>>value of type 'B' can be used anywhere a value of type 'A' is expected
>>without changing the behavior of a program, '
> 
> 
> Where did you get the "without changing the behavior of a program"? 

http://burks.brighton.ac.uk/burks/foldoc/20/67.htm

> Obviously the point of polymorphism is that the behaviour will change.

And just as obviously, this violates LSP as originally stated--hence the 
OP's question.

Note that this is not to argue that virtual functions in C++ are wrong 
or bad. It is to argue that the LSP is not *by itself* a foundation for OO.


>>then 'B' is a subtype of
>>'A'. The problem is of course that if 'B' doesn't change anything, why
>>distinguish it from 'A' in the first place?
> 
> 
> Even granting the other, the point would be that B can have additional 
> functionality that is used when treating it as a B, but not used when 
> treating it as an A.

Yes, it's hard to imagine LSP being meaningful at all without at least this.

-thant


      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Thant 10/17/2003 4:48:28 PM

Thant Tessman wrote:
> Aaron Bentley wrote:
> 
>>Thant Tessman wrote:
> 
> 
> [...]
> 
> 
>>>Actually, RTTI *does* violate LSP as it was originally stated: if a
>>>value of type 'B' can be used anywhere a value of type 'A' is expected
>>>without changing the behavior of a program, '
>>
>>
>>Where did you get the "without changing the behavior of a program"? 
> 
> 
> http://burks.brighton.ac.uk/burks/foldoc/20/67.htm

Thanks for the link.  I hadn't read that before.  Let's get the original 
formulation quoted here:

If for each object o1 of type S there is an object o2 of type T such 
that for all programs P defined in terms of T, the behaviour of P is 
unchanged when o1 is substituted for o2 then S is a subtype  of T.

>>Obviously the point of polymorphism is that the behaviour will change.
> 
> 
> And just as obviously, this violates LSP as originally stated--hence the 
> OP's question.

I tend to agree.  Looks like Liskov's description of a subtype is 
stricter than the C++ version.  And it looks like the Howe 
interpretation ignores the key "the behavior is unchanged" bit, and so 
draws incorrect conclusions.  (Note that the OP was talking about RTTI 
though.)

Aaron
-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/18/2003 9:46:05 PM

Aaron Bentley <aaron.bentley@utoronto.ca> wrote in message news:<WsQjb.5940$Z_2.467032@news20.bellglobal.com>...

 > Obviously the point of polymorphism is that the behaviour will change.

Ah, but that's not the point at all. :)

As per the C++ FAQ[1], subtype polymorphism is about allowing old code
to call new code. Old code specifies behavior; new code specifies only
how it will implement that behavior. If the new code behaves
differently than the old code expects, the protocol breaks down.

- Shane

1. http://www.parashift.com/c++-faq-lite/big-picture.html#faq-6.9

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply sbeasley 10/19/2003 1:11:25 PM

Shane Beasley wrote:
> Aaron Bentley <aaron.bentley@utoronto.ca> wrote in message news:<WsQjb.5940$Z_2.467032@news20.bellglobal.com>...
> 
>  > Obviously the point of polymorphism is that the behaviour will change.
> 
> Ah, but that's not the point at all. :)
> 
> As per the C++ FAQ[1], subtype polymorphism is about allowing old code
> to call new code. Old code specifies behavior; new code specifies only
> how it will implement that behavior. If the new code behaves
> differently than the old code expects, the protocol breaks down.

The bit about old code specifying behaviour is not in the FAQ, and I 
disagree with it.  The old code specifies operations that the new code 
must support, but doesn't prescribe what those operations should do.

So long as the behaviour with respect to the old code is correct, the 
protocol will not break down, but "behaviour" is much more than 
"behaviour-with-respect-to-old-code".

If there is no difference in interface or behaviour betweeen two 
classes, then why have two classes?

Aaron
-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/19/2003 9:46:16 PM

Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
ueykb.64$aw5.17248@news20.bellglobal.com...
> Shane Beasley wrote:
> > Aaron Bentley <aaron.bentley@utoronto.ca> wrote in message
news:<WsQjb.5940$Z_2.467032@news20.bellglobal.com>...
> >
> >  > Obviously the point of polymorphism is that the behaviour will
change.
> >
> > Ah, but that's not the point at all. :)
> >
> > As per the C++ FAQ[1], subtype polymorphism is about allowing old code
> > to call new code. Old code specifies behavior; new code specifies only
> > how it will implement that behavior. If the new code behaves
> > differently than the old code expects, the protocol breaks down.
>
> The bit about old code specifying behaviour is not in the FAQ, and I
> disagree with it.  The old code specifies operations that the new code
> must support, but doesn't prescribe what those operations should do.
>

To be more precise, the old code does not prescribe what should happen
"under the hood", it only prescribes what should "appear" to happen. In
other words, it expects a certain input to lead to a certain result, no
matter what algorithm the new code choses.

> So long as the behaviour with respect to the old code is correct, the
> protocol will not break down, but "behaviour" is much more than
> "behaviour-with-respect-to-old-code".
>
> If there is no difference in interface or behaviour betweeen two
> classes, then why have two classes?
>

Because the same behaviour can be accomplished in different ways. The two
classes chose different ways.

Regards,

Matthias



      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Matthias 10/21/2003 3:25:26 PM

My interpretation is that the behavior of the program does not change
because of an algorithm changes. Let me explain. You can have the
classic
hierachy of Shape that has a Circle and Rectangle classes derived from
it.
The driver program prints a report where it shows the Shape's ID and the
Shape's area. So you basically have something like this:

class Shape
{
public:
    int id () const = 0;
    int area () const = 0;
    // Whatever else you need.
}

class Circle : public Shape
{
    // Overwritte id and area
}

class Rectangle : public Shape
{
    // Overwritte id and area
}

So now, your driver program looks something like this.

int main ()
{
    std::vector<Shape*> MyShapes;
    getMyShapes(MyShapes)
    std::vector::const_iterator iter = MyShapes.begin();
    for (; iter != MyShapes.end(); ++iter) {

        std::cout << iter->id() << " " << iter->area() << std::endl;
    }

    return 0;
}

The algorithm to calculate the area of a circle is different than the
algorithm to calculate the area for a rectangle. That's obvious. But the
semantics of the area() method do not change, so the program's behavior
would not change. It would be different if we overwritte the area()
method
of the Rectangle class to return the height of the rectangle instead of
the
area. That would violate LSP. IMO, a change of algorithm not necessarily
implies a change in program behavior. It may mean a change in object
behavior, but we always want to maintain the semantics intact.

-- 
Regards,

Isaac Rodriguez
=======================
Software Engineer - Autodesk



      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Isaac 10/22/2003 1:25:40 AM

Matthias Hofmann wrote:
> Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag: 
> ueykb.64$aw5.17248@news20.bellglobal.com...
> 

[...]

>> If there is no difference in interface or behaviour betweeen two 
>> classes, then why have two classes?
> 
> 
> Because the same behaviour can be accomplished in different ways. The
> two classes chose different ways.

Consider the pedagogically ubiquitous Shape class with a virtual
function "draw" supplied by the sibling descendents Circle and Square.
You may want to argue that each sibling is distinguised not by behavior,
but by implementation. However, it seems more reasonable to think of
each as as implementing different behaviors invoked by a single but
context-sensitive message "draw."

Virtual functions may facilitate data abstraction in a more traditional
"Liskovian" sense, but run-time type dispatching deserves consideration
independent of OO's prejudices.

-thant


      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Thant 10/22/2003 1:30:35 AM

Matthias Hofmann wrote:
> Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
> ueykb.64$aw5.17248@news20.bellglobal.com...
> 
>>Shane Beasley wrote:
>>
>>>Aaron Bentley <aaron.bentley@utoronto.ca> wrote in message
> 
> news:<WsQjb.5940$Z_2.467032@news20.bellglobal.com>...
> 
>>> > Obviously the point of polymorphism is that the behaviour will
> 
> change.
> 
>>>Ah, but that's not the point at all. :)
>>>
>>>As per the C++ FAQ[1], subtype polymorphism is about allowing old code
>>>to call new code. Old code specifies behavior; new code specifies only
>>>how it will implement that behavior. If the new code behaves
>>>differently than the old code expects, the protocol breaks down.
>>
>>The bit about old code specifying behaviour is not in the FAQ, and I
>>disagree with it.  The old code specifies operations that the new code
>>must support, but doesn't prescribe what those operations should do.
>>
> 
> 
> To be more precise, the old code does not prescribe what should happen
> "under the hood", it only prescribes what should "appear" to happen. In
> other words, it expects a certain input to lead to a certain result, no
> matter what algorithm the new code choses.

So, for example you could have BubbleSorter::doSort(...) and 
QuickSorter::doSort() be implemtations of the same virtual function.

They have the same inputs and outputs, but are implemented differently.

The only problem with that is that Quick Sort is a superior algorithm, 
and while you might use BubbleSorter for debugging, there are very few 
situations where you want to write a program that chooses the sorting 
algorithm at runtime.

On the other hand, Square::draw() and Circle::draw() can be 
implementations of the same virtual function (say 
DrawableObject::draw()), and there are plenty of situations where you 
want to draw a collection of DrawableObjects.  But this use of 
polymorphism is not included in your definition, because the differences 
are not under the hood.

>>If there is no difference in interface or behaviour betweeen two
>>classes, then why have two classes?
>>
> 
> 
> Because the same behaviour can be accomplished in different ways. The two
> classes chose different ways.

If there's no observable difference, you should pick the superiour class 
and just use that.  Having two codepaths that do the same thing just 
doubles your work for no appreciable gain.

Aaron

-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/22/2003 6:30:02 PM

Thant Tessman <thant@acm.org> schrieb in im Newsbeitrag:
bn3u7s$pvr$1@terabinaries.xmission.com...
> Matthias Hofmann wrote:
> > Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
> > ueykb.64$aw5.17248@news20.bellglobal.com...
> >
>
> [...]
>
> >> If there is no difference in interface or behaviour betweeen two
> >> classes, then why have two classes?
> >
> >
> > Because the same behaviour can be accomplished in different ways. The
> > two classes chose different ways.
>
> Consider the pedagogically ubiquitous Shape class with a virtual
> function "draw" supplied by the sibling descendents Circle and Square.
> You may want to argue that each sibling is distinguised not by behavior,
> but by implementation. However, it seems more reasonable to think of
> each as as implementing different behaviors invoked by a single but
> context-sensitive message "draw."

One major issue of this discussion seems to be what "behaviour" actually
means. In your example, one might say that Circle and Square differ in
behaviour as the output is different. On the other hand, their behaviour is
*conceptually* identical as they do the same thing - draw their outline on
the screen.

Let's take another example: A class "AudioFile" with a virtual function
"Play()", which is supplied by the sibblings WavFile and Mp3File. Obviously,
the algorithm for playing a wave file is different from the algorithm that
plays an mp3 file. However, you won't notice a difference in behaviour when
you use these classes (assuming you can't hear the difference in sound
quality). This, I think, is an even better example for old code specifying
behaviour of new code. But the Shape example also works to express the idea,
as printing a different shape probably won't break the old code.

>
> Virtual functions may facilitate data abstraction in a more traditional
> "Liskovian" sense, but run-time type dispatching deserves consideration
> independent of OO's prejudices.
>

Of course it is possible to implement a virtual function "draw" that
actually erases your hard disk. As usual, nobody keeps you from shooting
yourself in the foot if you really want to. I wonder what Liskov means by
"changing the behaviour of the programm".

As for the original post: I think that RTTI does not violate the LSP as it
says nothing about the code *knowing* the real type of an object.

Regards,

Matthias




      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Matthias 10/22/2003 6:34:38 PM

Matthias Hofmann wrote:

[...]

> One major issue of this discussion seems to be what "behaviour" actually
> means. In your example, one might say that Circle and Square differ in
> behaviour as the output is different. On the other hand, their behaviour is
> *conceptually* identical as they do the same thing - draw their outline on
> the screen.

Whether one behavior is "conceptually" identical to another is a notion 
that's pretty hard to pin down. And I seriously doubt Liskov had this in 
mind when she phrased her principle of subtyping. My point is: so what? 
The validity of run-time type dispatching (or lack thereof) doesn't have 
to depend on its fidelity (or lack thereof) to the LSP. Or vice versa 
for that matter. More than that, I claim the tendency to equate LSP to 
object-oriented to "good design" by stretching the definitions of words 
like "behavior" is counter-productive. It weakens our ability to reason 
more formally about type systems.

[...]

-thant


      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Thant 10/23/2003 11:25:47 AM

Matthias Hofmann wrote:
 > Thant Tessman <thant@acm.org> schrieb in im Newsbeitrag:
 > bn3u7s$pvr$1@terabinaries.xmission.com...
 >
 > One major issue of this discussion seems to be what "behaviour" actually
 > means. In your example, one might say that Circle and Square differ in
 > behaviour as the output is different. On the other hand, their behaviour is
 > *conceptually* identical as they do the same thing - draw their outline on
 > the screen.

That's right-- I (and apparently Thant) feel that
"behaviour-with-respect-to-old-code" or with respect to a given
interface is only a subset of behaviour.

 > Let's take another example: A class "AudioFile" with a virtual function
 > "Play()", which is supplied by the sibblings WavFile and Mp3File. Obviously,
 > the algorithm for playing a wave file is different from the algorithm that
 > plays an mp3 file. However, you won't notice a difference in behaviour when
 > you use these classes (assuming you can't hear the difference in sound
 > quality). This, I think, is an even better example for old code specifying
 > behaviour of new code. But the Shape example also works to express the idea,
 > as printing a different shape probably won't break the old code.

I think WavFile would probably signal an error if presented with an mp3
file, and I imagine MP3File would probably signal an error if presented
with a wav file.  To me, this is an obvious difference in behavour.

 > Of course it is possible to implement a virtual function "draw" that
 > actually erases your hard disk. As usual, nobody keeps you from shooting
 > yourself in the foot if you really want to. I wonder what Liskov means by
 > "changing the behaviour of the programm".

Definition of "behavour" again.  I hold that a program that erases your
hard disk has different behavour from a program that draws shapes.  Your
agument seems to be that "behaviour" means something different.

 > As for the original post: I think that RTTI does not violate the LSP as it
 > says nothing about the code *knowing* the real type of an object.

Agreed.

-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/23/2003 6:53:34 PM

Since Thant used the same examples as I did, I'm putting most of my
replies in that thread.

Matthias Hofmann wrote:
 > ----- Original Message -----
 > From: Aaron Bentley <aaron.bentley@utoronto.ca>
 > Newsgroups: comp.lang.c++.moderated
 > Sent: Wednesday, October 22, 2003 8:30 PM
 > Subject: Re: RTTI v.s. Liskov Principle
 >>there are very few
 >>situations where you want to write a program that chooses the sorting
 >>algorithm at runtime.
 >>
 >
 >
 > But it could be possible that a better algorithm is discovered later, after
 > the programm calling doSort() is written. Then it would make sense to have
 > the sorting function be virtual.

But from then on, you'll *always* prefer BetterSort.  So you can choose
the sorting algorithm at compile time instead of at runtime.

[ snip shape example & discussion & old stuff]

 >>If there's no observable difference, you should pick the superiour class
 >>and just use that.  Having two codepaths that do the same thing just
 >>doubles your work for no appreciable gain.
 >>
 >
 >
 > I do not agree with that. Take a look at my other post about the AudioFile
 > for an example where there is no such thing as a superior class.

[ see also my post to that thread]

I hold that the AudioFile classes have observable differences, e.g. one
can play wav files, and the other cannot.

Aaron

-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/23/2003 6:54:19 PM

Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
6qPlb.4335$aw5.328797@news20.bellglobal.com...
 > Matthias Hofmann wrote:
 >  > Thant Tessman <thant@acm.org> schrieb in im Newsbeitrag:
 >  > bn3u7s$pvr$1@terabinaries.xmission.com...
 >  >
 >  > One major issue of this discussion seems to be what "behaviour"
actually
 >  > means. In your example, one might say that Circle and Square differ in
 >  > behaviour as the output is different. On the other hand, their
behaviour is
 >  > *conceptually* identical as they do the same thing - draw their outline
on
 >  > the screen.
 >
 > That's right-- I (and apparently Thant) feel that
 > "behaviour-with-respect-to-old-code" or with respect to a given
 > interface is only a subset of behaviour.
 >
 >  > Let's take another example: A class "AudioFile" with a virtual function
 >  > "Play()", which is supplied by the sibblings WavFile and Mp3File.
Obviously,
 >  > the algorithm for playing a wave file is different from the algorithm
that
 >  > plays an mp3 file. However, you won't notice a difference in behaviour
when
 >  > you use these classes (assuming you can't hear the difference in sound
 >  > quality). This, I think, is an even better example for old code
specifying
 >  > behaviour of new code. But the Shape example also works to express the
idea,
 >  > as printing a different shape probably won't break the old code.
 >
 > I think WavFile would probably signal an error if presented with an mp3
 > file, and I imagine MP3File would probably signal an error if presented
 > with a wav file.  To me, this is an obvious difference in behavour.
 >
 >  > Of course it is possible to implement a virtual function "draw" that
 >  > actually erases your hard disk. As usual, nobody keeps you from
shooting
 >  > yourself in the foot if you really want to. I wonder what Liskov means
by
 >  > "changing the behaviour of the programm".
 >
 > Definition of "behavour" again.  I hold that a program that erases your
 > hard disk has different behavour from a program that draws shapes.  Your
 > agument seems to be that "behaviour" means something different.

Yes, that is my point. And I will try to give a definition for behaviour,
the way I understand it. It actually represents the idea of "design by
contract":

The behaviour of a function is defined by a set of preconditions and
postconditions. The function behaves as expected if it meets those
preconditions and postconditions. If the function is virtual, its overrides
in derived classes might be able to handle more restrictive preconditions,
but it may not less restrictive preconditions. As for the postconditions, a
virtual function's overrides in derived classes may comply with even
stricter postconditions, but not with less restrictive ones.

Example:

class FileManager
{
public:
     virtual FILE* OpenFile( const char* name )
     {
         if ( /* File exists */ )
         {
             // Open file and return pointer.
         }
         return NULL;
     }
};

class AdvancedFileManager : public FileManager
{
public:
     virtual FILE* OpenFile( const char* name )
     {
         if ( name != NULL ) // Handle stricter precondition
         {
             if ( /* File exists */ )
             {
                 // Open file and return pointer.
             }
             else
             {
                 // Comply with stricter postcondition.
                 // Create file and return pointer.
             }
         }
     }
}

void f( FileManager& fm )
{
     // Preconditions: The passed string must not be NULL.

     FILE* p = fm.OpenFile( "test.txt" );

     // Postconditions: The return value must be a file pointer or NULL.
}

I don't know if this is the best of all examples, but it was really
difficult for me to find one that demonstrates what I am trying to say. The
FileManager class supplies a virtual function OpenFile(). It's behaviour is
defined by the following preconditions and postconditions: It takes a string
for the file name, and this string must not be a NULL pointer. The file's
name does not matter. It shall
return a pointer to an open file or NULL.

The overridden version of OpenFile() in AdvancedFileManager also meets those
preconditions and postconditions. In order to demonstrate what I want to say
with my definition of a function's behaviour, I am showing that it can also
handle the case where the passed pointer is NULL, which means it complies
with even stricter preconditions. However, it cannot assume less restrictive
preconditions, for example that it only needs to handle "*.txt" files.

As for the postconditions, AdvancedFileManager::OpenFile() also complies
with those - it even meets with more restrictive postconditions by *always*
returning a pointer to a file (for the sake of simplicity, I did not handle
the case where the file could not be created). However, it cannot assume
less restrictive postconditions, e.g. it cannot (stupid example, I know) use
"rand()" to generate a file pointer.

The functions or better say the two classes differ in behaviour in as much
as one of them creates a file if it does not exist. However, considering the
fact that they comply with certain preconditions and postconditions, I'd say
they both behave as expected.

What do you think about this definition of behaviour?

 >
 >  > As for the original post: I think that RTTI does not violate the LSP as
it
 >  > says nothing about the code *knowing* the real type of an object.
 >
 > Agreed.
 >








      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Matthias 10/24/2003 7:28:13 PM

Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
3F97C408.40304@utoronto.ca...
 > Since Thant used the same examples as I did, I'm putting most of my
 > replies in that thread.
 >

  [snip]

 >
 > I hold that the AudioFile classes have observable differences, e.g. one
 > can play wav files, and the other cannot.
 >

I guess I should have expressed myself a bit more clearly about the audio
files. There definitely *is* a diference between them, but it does not
necessarily have to be *observeable*. Let's say in the old code, there is a
function like the following one:

void PlayAudioFile( AudioFile& af )
{
     af.Play();
}

When the AudioFile is passed to the function, it is already initialized and
ready to be played. This initialization will most likely take place in code
that is newer and knows the exact type (i.e. the dynamic type) of the audio
file object whose reference is passed.

However, I am just noticing an error in my argumentation. The definition of
the LSP, as I interpret it, requires the objects to be replaceable in every
place of the code, not just in certain parts of it, like for the call of the
function above. As you cannot initialize a WavFile object with an Mp3 file,
this requirement is not met... :-(

Best regards,

Matthias




      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Matthias 10/24/2003 7:28:49 PM

Matthias Hofmann wrote:
 > Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
 > 3F97C408.40304@utoronto.ca...
  >
 >  > I hold that the AudioFile classes have observable differences, e.g. one
 >  > can play wav files, and the other cannot.
 >  >
 >
 > I guess I should have expressed myself a bit more clearly about the audio
 > files. There definitely *is* a diference between them, but it does not
 > necessarily have to be *observeable*. Let's say in the old code, there is a
 > function like the following one:
 >
 > void PlayAudioFile( AudioFile& af )
 > {
 >      af.Play();
 > }

See I had assumed that AudioFile was used this way:
void PlayAudioFile( const char *filename )
{
     for (vector<AudioFile *>::iterator it=audiofiles.begin();
it!=audiofiles.end();  ++it)
        if ((*it)->load(filename))
        {
            (*it)->play();
            return;
        }
     throw AudioFile::NoSuitableDecoder(filename);
}

 > When the AudioFile is passed to the function, it is already initialized and
 > ready to be played. This initialization will most likely take place in code
 > that is newer and knows the exact type (i.e. the dynamic type) of the audio
 > file object whose reference is passed.

As shown above, you don't need new code for that.  In fact, my
AudioFiles bear some resemblence to Winamp plugins-- which aren't even
compiled on the same machine as Winamp.

 > However, I am just noticing an error in my argumentation. The definition of
 > the LSP, as I interpret it, requires the objects to be replaceable in every
 > place of the code, not just in certain parts of it, like for the call of the
 > function above. As you cannot initialize a WavFile object with an Mp3 file,
 > this requirement is not met... :-(

-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/25/2003 8:35:18 AM

Matthias Hofmann wrote:

 > Aaron Bentley <aaron.bentley@utoronto.ca> schrieb in im Newsbeitrag:
 >  > I hold that a program that erases your
 >  > hard disk has different behavour from a program that draws shapes.  Your
 >  > agument seems to be that "behaviour" means something different.
 >
 > Yes, that is my point. And I will try to give a definition for behaviour,
 > the way I understand it. It actually represents the idea of "design by
 > contract":
 >
 > The behaviour of a function is defined by a set of preconditions and
 > postconditions. The function behaves as expected if it meets those
 > preconditions and postconditions. If the function is virtual, its overrides
 > in derived classes might be able to handle more restrictive preconditions,
 > but it may not less restrictive preconditions. As for the postconditions, a
 > virtual function's overrides in derived classes may comply with even
 > stricter postconditions, but not with less restrictive ones.
 >

I'm familiar with DbC, though I haven't used it in my work yet.  DbC is
not directly supported by C++, and is certainly not an accurate
description of C++ polymorphism.  (Though it is, no doubt, a good
guideline for using polymorphism.)  There was recently a discussion here
about the best way to implement DbC in C++.

 > Example:
 >
 > class FileManager
 > {
 > public:
 >      virtual FILE* OpenFile( const char* name )
 >      {
 >          if ( /* File exists */ )
 >          {
 >              // Open file and return pointer.
 >          }
 >          return NULL;
 >      }
 > };
 >
 > class AdvancedFileManager : public FileManager
 > {
 > public:
 >      virtual FILE* OpenFile( const char* name )
 >      {
 >          if ( name != NULL ) // Handle stricter precondition
 >          {
 >              if ( /* File exists */ )
 >              {
 >                  // Open file and return pointer.
 >              }
 >              else
 >              {
 >                  // Comply with stricter postcondition.
 >                  // Create file and return pointer.
 >              }
 >          }
 >      }
 > }


 >
 > void f( FileManager& fm )
 > {
 >      // Preconditions: The passed string must not be NULL.
 >
 >      FILE* p = fm.OpenFile( "test.txt" );
 >
 >      // Postconditions: The return value must be a file pointer or NULL.
 > }
 >
 > I don't know if this is the best of all examples, but it was really
 > difficult for me to find one that demonstrates what I am trying to say. The
 > FileManager class supplies a virtual function OpenFile(). It's behaviour is
 > defined by the following preconditions and postconditions: It takes a string
 > for the file name, and this string must not be a NULL pointer. The file's
 > name does not matter. It shall
 > return a pointer to an open file or NULL.
 >
 > The overridden version of OpenFile() in AdvancedFileManager also meets those
 > preconditions and postconditions. In order to demonstrate what I want to say
 > with my definition of a function's behaviour, I am showing that it can also
 > handle the case where the passed pointer is NULL, which means it complies
 > with even stricter preconditions. However, it cannot assume less restrictive
 > preconditions, for example that it only needs to handle "*.txt" files.

One stricter postcondition would be that the opened file must be a given
type, e.g. a text file, and in that case, AdvancedFileManager would
return NULL more often than FileManager.

 > As for the postconditions, AdvancedFileManager::OpenFile() also complies
 > with those - it even meets with more restrictive postconditions by *always*
 > returning a pointer to a file (for the sake of simplicity, I did not handle
 > the case where the file could not be created). However, it cannot assume
 > less restrictive postconditions, e.g. it cannot (stupid example, I know) use
 > "rand()" to generate a file pointer.
 >
 > The functions or better say the two classes differ in behaviour in as much
 > as one of them creates a file if it does not exist. However, considering the
 > fact that they comply with certain preconditions and postconditions, I'd say
 > they both behave as expected.
 >
 > What do you think about this definition of behaviour?

It's pretty restrictive.  Liskov's principle talks about the behaviour
of *programs*, not functions or sets of functions.  You'll also find
that most discussion here uses "behaviour" in a more wholistic sense
that includes whether the program crashes, or destroys the data on a
hard disk, or goes into an infinite loop.  The C++ standard uses
"undefined behavior" to mean "anything can happen", and "unspecified
behavior" to mean "any one of these things can happen".

I'm sure your narrowed terminology is useful in some contexts, but using
it to prove that Liskov includes polymorphism  seems like unnecessary
contortions.  It would be far easier to say that non-polymorphic C++
subtypes obey the Liskov principle, but polymorphic subtypes often don't.

Aaron

-- 
Aaron Bentley
www.aaronbentley.com

      [ See http://www.gotw.ca/resources/clcm.htm for info about ]
      [ comp.lang.c++.moderated.    First time posters: Do this! ]
0
Reply Aaron 10/25/2003 8:36:44 AM

21 Replies
214 Views

(page loaded in 0.195 seconds)


Reply: