|
Boost : |
From: Aaron W. LaFramboise (aaronrabiddog51_at_[hidden])
Date: 2005-03-07 03:19:54
Mathew Robertson wrote:
> I'd argue that we know exactly - no more, no less -> either the
> operating system supports a capability, or it doesn't. The only
> real question is, 'when do we detect the capabilities,
> at build time? at run time?'
I think we need more granularity than this. One important
counterexample is the case when a capability may be enabled while the
operating system is running. For example, a driver may be loaded that
presents a high-quality random number source.
>>Heres another one of my favorite examples, in Boost.Threads
>>(Boost.Threads, <http://www.boost.org/doc/html/threads.html>).
>>boost::thread lacks a method to forcefully terminate a thread, despite
>>the fact that many threading systems have one.
>
> So most people go with an implementation that does not provide forcible termination.
OK. I'm not concerned with this particular feature, but the fact that
there will always be some feature that the system supports that a user
may want that Boost.Threads will not implement. If its not this, then
it might be thread priority, and if not that, then it might be setting a
thread's processor affinity, or who knows what else.
My point is that the concrete Boost.Thread is not open for extension.
If you want a feature it doesn't provide, you either have to modify the
sources, or just write yourself an entirely new thread ibrary. This is
should not be the way we do it in C++.
>>Besides the inability of our concrete interfaces to support a capability
>>set that varies,
>
> The Boost concrete interfaces only provide non-varying implementations
> so that the code is predictable on all supported platforms. Would you
> prefer that you would need to test your code on every revision of
> every platform?
With the polymorphic scheme I propose, if you only want the least common
denominator interface, then thats all you have to use, and there is no
changes needed to your testing procedure.
Furthermore, static, compile-time checking can still be achieved by
using concrete factories. For example, if you'd like compilation to
fail if POSIX facilities are unavailible, code like this could be written.
Process p = POSIXProcessFactory::getProcess("some_task");
p->getProcPath();
> Its current form is a result of understanding the fundamental problems:
> - single reasonable useful API
> - an API that works across most platforms
> - an implementation that is predictable across the supported platforms
I am not proposing altering these guarantees--and for what it's worth, I
agree these are very important characteristics of Boost libraries.
However, I am proposing that in addition to providing this basic,
portable, guaranteed interface, we allow the user to 'upcast' to
system-specific interfaces, with compile-time or runtime checking, at
their option.
> The proc filesystem is a directory structure - not a POSIX definition - the comparison is invalid.
>
> In the example of a debugger, what does the debuggin process do if there is no debugging capabilities for that platform?
> Again the problem is "at what point do we detect the available capabilities".
Presumably the factory/copy operation fails, and the debugger has to
cope with this situation. This is unavoidable in any case, regardless
of what abstraction is being used, as it is impossible to determine if
/proc is mounted at compile-time.
>>Im calling for polymorphism. These interfaces really are conceptually
>>polymorphic; lets reflect that in our language. Lets give the user
>>the tools she needs to be able to write her own compatible classes when
>>the ones we write prove insufficient.
>
> I'd argue that the capabilities arn't polymorphic. ie you simply couldn't do somthing like this:
>
> class Process {
> virtual void run();
> }
>
Let me re-arrange your example slightly.
class Process_with_slash_proc : virtual public Process {
// ...
};
Process p = ProcessFactory::getProcess("some_task");
try {
Process_with_slash_proc p2 = Clone_process_with_slash_proc(p);
p2->getProcPath();
}
catch(something &) {
// ...
}
This sort of runtime detection would presumably by a rare case, but its
an important case! This system also allows us to do things like load a
DLL at runtime that knows how to perform some sort of operation on a
Process that the user, or the original author of Process, doesn't even
know about.
A more common case:
class signalable_process : virtual public Process {
public:
int send_signal(int);
};
class POSIX_process : public signalable_process /* , ... */ {
// ...
};
POSIX_Process p = POSIXProcessFactory::getProcess("some_task");
p.send_signal(42);
Again, this is just an naïve example. While this isn't identical to a
real design, it shows how we can reflect capability sets through
inheritance. One could imagine having an HPUX class that derived from
the POSIX class that presented additional HPUX-specific interfaces.
> So that you essentially get objects which will have the same
> generic / portable' API, but you also get the OS specific
> functionality.
Exactly! This is exactly what I'm getting at.
> Then the programmer decides to use that OS specific functionality.
> The end result? Non-portable code.
The programmer deliberately chose to use this non-portable
functionality, and presumably he knew exactly what he was doing. C++'s
type safety leaves very little room for accidents.
> Why doesn't the programmer simply just the use the native API...? Why
> bother using Boost at all?
So he can reuse the Boost code. For example, the process class already
does 95% of what he needs. He just needs one other feature it's missing.
To me, this is the essence of re-use, and the open-closed principle, and
what C++ is supposed to be.
It's very odd to me that you think it would be better to refuse to allow
the programmer to trivially extend the class, and instead force him to
reimplement the entire process library.
Besides that, the ability to extend in this manner leads to a common
framework. Unlike the bad kind of framework that tends to lock people
out more than anythin else, this kind of framework provides common
ground on which people may make separate, but compatible, extensions.
For instance, Katie might implement a class that sends signals to a
process, and Amy might implement a class that waits for process
termination, and Nicole could use both of these capabilities together,
on the same process, at the same time. This is reuse.
> I dont understand... doesn't:
>
> "A more significant concern is the additional machinery needed to support virtual inheritance."
>
> conflict with:
>
> "Im not at all worried about making operations that would be normal function calls into virtual function calls..."
In the former quote, I'm talking about the cost associated with doing
upcasts with dynamic_cast, and similar operations. In the latter, I'm
talking about the simple indirect calls.
For someone who only chooses to use the least common denominator
functionality, they will only need to be concerned with the latter sort
overhead.
>> Would you use a library such as Boost.Thread if it had been rewritten in this manner?
>
> No - it is not deterministic.
I don't understand your objection. If you choose to use only the least
common denominator functionality, I'd expect the behavior to be,
formally and practically, identical to the status quo concrete class.
However, for those users who need features only availible in some
environments, they are free to re-use the existing code, plus add their
own code to support their particular use-case.
I can't beleive that it is better to *not* give a user a chance at
extending the class.
Thank you for the detailed analysis and criticism,
Aaron W. LaFramboise
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk