From: Jeff Garland (jeff_at_[hidden])
Date: 2005-02-27 14:45:08
On Sun, 27 Feb 2005 17:52:23 +0000 (UTC), Bob Bell wrote
> Jeff Garland <jeff <at> crystalclearsoftware.com> writes:
>...snip time_duration example...
> > Again, we've derived from a class where we don't want virtual functions, we do
> > want to substitite the base class, and it is totally safe because of no
> > allocation / trivial destructor in the sub-class.
> Given that you've got these classes called "seconds" and
> "milliseconds", I'd expect to be able to do things like:
> seconds s = seconds(10) + milliseconds(20);
> milliseconds ms = seconds(10);
> ++s; // increment one second
> ++ms; // increment one millisecond
> void foo(seconds);
> time_duration td(/* ... */);
> Are these things supported?
No, because time_duration doesn't support operator++ or operator--. And
actually with the class design it would be pretty easy to extend the
subclasses to support this even without offering it in general.
> They are all, "code with obvious meaning,
> " (I suppose they could be supported, I don't really know.
"It's not obvious" is the main reason that operator++/-- aren't supported in
date-time. You would have to know the resolution of the time_duration type to
understand the code. And since that's a compilation option it seems
inherently dangerous to provide operator++/--. Not to mention hard to
understand. And really I think:
td += milliseconds(10);
isn't much more verbose than ++td.
> If all you want is to support the creation of simple time units, why
> not have named functions:
> time_duration seconds(int secs)
> return time_duration(0, 0, secs, 0);
> time_duration milliseconds(int msecs)
> return time_duration(0, 0, 0, msecs);
> We can still write:
> //code with obvious meaning
> time_duration td = seconds(10) + milliseconds(10);
> But no additional classes are needed, and none of the corner cases come
> up or need to be supported.
Sure that probably works, but is there a corner case where it doesn't work?
Chained operations maybe? I don't know. In the inheritance design I know it
can always converted time_duration. As long as I write interfaces in terms of
time_durations all these little adapters can be used in place of the more
abstract case. To me this is 'isa' and I don't know of a 'corner case' where
this design creates a problem. Another thing. Now that I have an actual type
called seconds I can write a function interface or have a data member in terms
of that type if I have a reason to be specific about resolution. With a
function I can't do that. To me this turns out to be the key for having a
type instead of a function.
Anyway, I'd be happy to learn if a real actual corner case exists for this
design (that's part of the reason I posted). I don't see it, but there are
far smarter people than I that read this list...
> IMHO, inheritance is often too broad a brush. You get the effect
> you're after, plus other effects you may not have wanted or
> anticipated. It's usually a good idea to use as fine a brush as you
> can get, and that means that if there's some way to implement what
> you want without using inheritance, you should probably not use inheritance.
Believe me, I agree that inheritence is easy to overuse. But 'broad brush'
versus 'fine brush' as a guideline doesn't work. You'll need to define those
terms precisely for anyone to use them as guidance. In the example above I
don't know how those terms apply. I've read Meyer's guidance and I'm choosing
to violate it because I don't think the problems he cite applys and I think
the design has advantages.
> > I'd say there is something about writing less code (not laziness)
> > that makes it a better design. Having to forward or reimplement
> > the whole interface of
> > an stl container is non-trivial and probably a bad design.
> I'd say that the achievement of writing (a little) less code at the
> expense of tighter coupling _is_ laziness.
But I WANT to tie my design to the standard library. Why, because it's very
stable and most C++ developers know about it. It will allow for seemless
client extensions without modification of my class. To me, depending on and
offering interfaces using the standard library is an example of a good design
> It's actually is pretty
> trivial to write forwarding functions of an standard container -- so
> trivial it wouldn't take long to write a code generator for it --
> especially when you consider that the whole interface is rarely required.
But in the real world every line of code written needs to be inspected,
tested, etc. 15 years from now someone will need to understand that code. I
really hate having to re-write interfaces and such -- it's just not productive.
> Which brings me to another point about inheriting from standard containers
> (I'm not sure if this applies to inheriting from other concrete
> types). In every case I can remember (including cases where I used
> to derive from containers myself), the desired result was a class
> that had a more restriced interface and/or behavior than the base
> (container) class. For example, creating a Polygon class by deriving
> from std::vector<Point>, then trying to limit the interface that
> only support the Polygon concept.
> In every case, achieving this kind of limiting turned out to be
> easier without using inheritance -- in fact, achieving this with
> public inheritance turns out to be pretty much impossible, so at
> some point you have to throw up your hands and decide to live with errors.
This is a bad use of inheritance and I agree inheritence shouldn't be used
here. In all my cases, I want a group of custom extensions to the collection
related to my domain problem and I want the full power of the collection
available to the client.
> One lesson I take from this, which I think does apply to (publicly) deriving
> from concrete classes is: when the semantics of the base class
> In my experience, this kind of alignment of semantics between a base
> concrete class and its derived class is rare. I don't know anything
> about the date_time library, but I'd be surprised if it were true
> for time_duration, seconds and milliseconds.
Well, I came to the conclusion that it is, but I'm open to being persuaded
otherwise. BTW in the real date-time library there are other things at play
including the fact that the resolution of the time_duration type is a
compilation option and hence the adjustment isn't as simple as I showed in the
example here. Since the time_duration type defines the resolution the
coupling is quite strong.
> > > If you want to inherit from, say, vector, do you _really_ want _all_
> > > its public member functions?
> > Yes, I frequently do.
> All I can say to this is that every programmer whom I've ever encountered
> who answered this question "yes" later changed his mind when we examined
> the entirety of vector's public interface.
> I won't go so far as to say "never" publicly derive from concrete
> classes or standard containers (I always want to leave myself a
> little wiggle room ;-), but I will say that in all the years I've
> worked with C++, I can't remember ever seeing a good example of it.
class FancyStringCollection : public std::vector<std::string>
//no added data...
I know, I should write all these functions domain related utilities as
standalone, but you know, most of them just aren't reuseable. They are
specific to a very small focus of the design related to the manipulation of
this specific collection type. And, I want the interfaces in terms of
std::vector<std::string> because I know it's stable and gives the client
plenty of options. Often times the extended collection isn't even used as an
interface class, but just as a helper to implement an encapsulating domain
class. By pulling out the collection related functions I simplify the domain
class code and can test the collection related operations independently.
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk