gCoord 3D Coordinates |
||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||||||||||||
Download: |
|
|||||||||||||||
The trouble with double |
Using floating point arithmetic is evil.
Digital Computers aren't very good at it, and programmers are often really awful at implementing floating point arithmetic sensibly.
The simplest example is Edit boxes in dialogs: if a user is typing in a floating-point number that is going to be stored (perhaps in the registry) and put back in the edit box, store the text!
If the text is converted to double and then back to text it will often have suffered from rounding errors: "5" becoming "4.99999999" etc. That's annoying for the user, but can be catastrophic when, after a few sums, there can be a rounding error which may be very small but can stop a program from working.
If a rounding error alters a number even slightly then comparisons may fail.
For example, if the result of a sum returns a value, d=1.00000001, this tiny variation from the value 1.0 will make the following condition fail:
if(d==1.0) DoSomething();
when the programmer didn't expect it to.
You can get around this by checking if the value is "nearly" 1.0 and the nearness (Tolerance) is used like this:
if(fabs(d-1.0)<Tolerance) DoSomething();
Tolerance is usually fairly similar between applications (0.0001 to 1e-6) and can usually be a very small number if necessary.
Most of the vector graphic projects I've been involved with use real-world coordinates in millimeters and a Tolerance of 0.0001 is adequate.
Clearly you don't want to be writing huge expressions every time you want to test the value of a coordinate,
so use a struct to hold a quantised double. Unfortunately C++ doesn't allow you to derive a class from the type "double", so the struct ends up with a lot of operators that help it model the behaviour of a double.
Since the main use for is to hold Coordinates, the class has been called gCoord. You may prefer QDouble, Quable etc.
gCoord has a double Coord; as its only member variable,
a Compare function (returning the usual -1, 0, +1 integer) which is used by all the comparison operators,
and uses a static sgn function where the Tolerance comes into play:
This leaves you with a type that you can use like a double, but comparisons will be handled quickly, efficiently and sensibly.
struct gCoord { // A quantised double. Quantisation Tolerance is handled in the sgn function: static int sgn(double d) {return d<-Tolerance ? -1 : (d>Tolerance);} double Coord; gCoord() {} gCoord(double d) : Coord(d) {} gCoord& operator= (const gCoord& c) {Coord= c.Coord; return *this;} bool operator> (const gCoord& c) const {return Compare(c)> 0;} [...all other operators for gCoord...] gCoord& operator= (const double d) {Coord= d; return *this;} bool operator> (const double d) const {return Compare(d)> 0;} [...all other operators for double...] gCoord& operator= (const int i) {Coord= i; return *this;} bool operator> (const int i) const {return Compare(i)> 0;} [...all other operators for int...] gCoord operator- () const {return -Coord;} bool operator! () const {return Compare(0)==0;} operator bool() const {return Compare(0)!=0;} operator double() const {return Coord ;} int Compare(double d) const {return sgn(Coord-d) ;} int sgn() const {return sgn(Coord) ;} }; [...all operators for doubles and ints taking gCoord& ...]Tolerance could be defined as a global const:
const double Tolerance=0.0001;
But it is within a separate class so that it can be temporarily altered.
Be aware that if you use the standard min and max macros (etc.) on these, then the Tolerance will be used...
This can introduce bugs if you let it!
For example:
gCoord A(1.000000001); A=acos(min(max(-1.0, A), 1.0)); // This will NOT clamp the value of A as you might expect!!!
To make sure that the normal, untoleranced double comparison is used, you should use:
A=acos(min(max(-1.0, A.Coord), 1.0)); A=acos(MinMax (-1.0, A.Coord , 1.0)); // This will work. Simple templated Min and Max are also provided in Global.h to avoid the macro versions recalculation issues A.Clamp(-1,1); // This followed by... A=acos(A); // ...this will work A=acos(A.Clamped(-1,1)); // This is what I would recommend!
Many programmers are unaware that PI and other common constants are defined in <math.h> for those that want them. I expose and add to them in Global.h which gCoord uses.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.