|
Boost : |
From: mkenniston_at_[hidden]
Date: 2001-11-24 14:02:11
I'd like to take another crack at this.
--- Topic 1
There was a question as to what's wrong with
(paraphrasing slightly):
namespace boost {
namespace math {
namespace float_constant {
double pi( 3.14159 );
} } }
It has already been pointed out that this doesn't
mesh well with generic programming, but it's actually
worse than that - this isn't easily extensible either,
even without bringing templates into it. Assume that
someone defines an arbitrary precision bigfloat
class. Then certainly users will want a corresponding
bigfloat pi. Even neglecting the mess that results
if bigfloat is itself a template class, we'll
end up with something like:
namespace boost {
namespace math {
namespace bigfloat_constant
bigfloat pi( appropriate_arguments );
} } }
The problem here is that since bigfloat may have a
non-trival constructor, that constructor may have to be
executed at runtime, and that opens up the whole
order-of-initialization can of worms. Let's regard
this as Yet Another Reason Why Globals Are Evil.
--- Topic 2
Peter Dimov pointed out that my previous idea, or any
similar scheme using convertible proxy objects that
are hidden from the user, has a problem with templates:
template<class Numeric> void foo(Numeric n);
foo(pi()); // calls void foo<some_type_that_is_not_numeric>
and Ed Brey raised a similar issue with:
double triple(double const* x); // Triples x, or returns 0 if x is
null.
The problem in each case is that "convertible to X" can
usually be substituted for "X", but not always. The really
nasty part is that the user interface pretends that they
are the same, so when the pretense fails (as it does in
these examples), the nature of the problem will often
be quite obscure to the end user and thus will result in
long and difficult debugging.
The subtlety (from the naive user's point of view) of this
kind of issue has been of growing concern to me. Now perhaps
one shouldn't argue against his own design like this, but
I've had to maintain existing systems and have learned to
be wary of "subtle". Then when Paul Bristow described
my design as "cunning" I got _really_ worried. :-)
--- Topic 3
"Once more unto the breach, dear friends, once more"
- Henry V, Act III, Scene I
Let's throw everything away (figuratively, not literally!)
and go back to first principles. The key observations are:
- Inline functions are a big win for the designer,
as they are very flexible and maximally efficient.
But (barring cunning tricks) function calls require
parentheses.
- Users refuse to write "pi()". Personally I
think this is silly, but WEB's users made this
point quite clear to him, so I must reluctantly
accept it. However, if I read his original post
http://groups.yahoo.com/group/boost/message/11442
correctly, the users didn't so much mind the extra
two characters of typing; instead they were objecting
to the "unnatural" notation. (Please correct me
if I'm wrong on this, since it's a crucial point.)
With this in mind, I'll propose another approach,
focusing on the fact that the constants of interest
are generally irrationals, and we'll handle the
parentheses problem by making a virtue of necessity.
In this design, we only have _one_ object called pi.
(Well, technically we can have lots of them, because
they're degenerate monostates, but the point is
that we do _not_ have separate "pi" objects or types
for float, double, long double, etc.)
Since this object is called "pi", we define it to
represent pi -- exactly. I mean that literally;
the object called "pi" will represent the precise,
100% accurate value of the irrational number,
with no rounding, approximation, or compromise.
Noah Stein was undoubtedly joking when he suggested
this, but I intend to show that it's actually quite
easy to do. Pretty cool, huh?
Now let's say a user who wants to calculate an
area says:
double radius;
cin >> radius;
double area;
area = pi * radius * radius;
This won't compile, because "pi" is an exact
representation of an irrational number, and we don't
have a CPU that can calculate with those. You have
to cast it to something you can compute with, like this:
area = double( pi ) * radius * radius;
Of course, we've re-introduced the parentheses, but
now we can explain what they do and why you need them in
terms that will make sense to a user. Furthermore, the
expression is essentially what it appears to be (a conversion
constructor), so any problems should be easy to diagnose.
The type of the expression is also explicit, so templates
and overload resolution and such will all work as one
would expect. Filling in the details (and assuming we
put everything in the appropriate namespace) we have:
// math.hpp
template< typename Tag >
struct irrational
{
template< typename Target >
operator Target() const;
};
struct pi_tag {};
typedef irrational< pi_tag > pi_t;
template<> template<>
inline irrational< pi_tag >::operator double() const
{
return 3.14159;
}
// math_constants.hpp
extern pi_t pi;
// math_constants.cpp
pi_t pi;
Comments:
- You can hack up a serviceable (but less extensible)
implementation with #ifdefs for template-challenged
compilers without messing up the user-visible interface.
- The "pi" identifier is provided for user convenience.
There should be no efficiency hit, because the actual
references to that identifier should be optimized
away, but it does require linking to a library binary.
If you are writing a library, you can use "pi_t()"
instead of "pi" to avoid introducing the link dependency.
- Because the conversions "double( pi )" are true
conversions that the compiler understands, I don't
think there will be any problems with using class names,
typedefs, or templates.
- There appears to be an order-of-initialization issue,
but since objects like pi don't have any state and their
(trivial default) constructors don't do anything, it doesn't
matter when, or even if, they get initialized.
- I got rid of the annoying null constructor by dropping
the "const" part of the declaration for the "pi" object.
Technically it should be a const, but since you can't do
anything to such an object anyway I don't see any harm
in leaving off the const, and it makes the compiler happy.
- I haven't yet pushed this to the example of a templated
user-defined type (like bigfloat), which would involve the
definition of a template class with a template conversion
operator member function whose template parameter is a
template class - but it sounds like a good test for
compiler compliance!
A further refinement:
The main drawback of this approach is that an explicit
conversion is required at every point of use of every
constant. I would expect that users will object to this,
but I'd also expect that the vast majority of uses are
inside arithmetic expressions. That means if we define
operators for + - * / like this:
template< typename Rep, typename Tag >
inline Rep operator*( irrational< Tag > lhs, Rep rhs )
{
return Rep( lhs ) * rhs;
}
then the user really can say:
area = pi * radius * radius;
There will still be a few places where an explicit conversion
will be required, but those should be sufficiently
few as to not be too annoying, and they can be explained
easily and clearly.
double radius, circumference;
cin >> radius;
circumference = 2.0 * pi * radius; // ok
circumference = 2 * ( pi * radius ); // ok
circumference = double( 2 ) * pi * radius; // ok
circumference = 2 * double( pi ) * radius; // ok
circumference = radius * pi * 2; // ok
circumference = 2 * radius * pi; // ok
circumference = 2 * pi * radius; // ERROR: int * irrational
or generically:
template< typename Rep >
Rep circumference( Rep radius )
{
return 2 * Rep( pi ) * radius;
}
If users really demand it, maybe something could be done to
make "2 * pi" work, but I fear that might get too cunning ...
Pros:
- very flexible for generic programming.
- allows arbitrary efficiency/accuracy tweaks.
- syntax of use is simple and transparent
(_how_ things are done can be hidden under
the covers, but _what_ is being done is
right out in the open).
- eliminates the need for a separate namespace
for each type.
- easy to extend to new constants (even if
incorporated into std::, since specialization
of classes in std:: is allowed).
- easy to extend to to new types (although I'm
a little hazy about whether the standard technically
allows a user to use template specialization to
overload a conversion operator member function of
a template class in std::).
Cons:
- occasional uses of constants must be explicitly
converted to double or whatever.
So, what do folks think? Will users buy this notation
and the rationale behind it, or will they just dismiss
it all as the wild rantings of a crazed designer? :-O
- Michael Kenniston
P.S. In situations like this, I often find I want to
specify "explicit" for conversion operators as well as
for conversion constructors. This seems logical to me,
since a conversion constructor in class A or a conversion
operator in class B both convert from B to A, yet the
language clearly allows only the constructor to be
declared "explicit". Does anyone know if this asymmetry
was deliberate, and if so what the reasoning behind it was?
Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk