
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(/* ... */); foo(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 coupling.
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 ...snip details 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.
[snip]
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> { public: some_domain_related_utility_function1() {...} some_domain_related_utility_function2() {...} //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. Jeff