Static char means what now?



  • A coworker was reading through some code, that apparently worked fine on SCO unix, but did not appear to work on Red Hat.

    He came across this little germ and shared it with me.
    You will need a little imagination, because I don't feel like re-creating the whole code.   This function was used all over in various programs.

    char *format_double(double dbl){
    static char string[20];
    memset(string, '\0', 20);

    // Do some work to turn a number ito a formatted string like 
    // 634529.04 ==> "$634,529.04"
    ... CODE FOR THAT ...

    return string;

    }

    And in dozens of places, we would have printf("%%FORMAT%%", format_double(val1), format_double(val2), format_double(val3), format_double(val4), format_double(val5));

    YAY!!!!!



  • @coplate said:


    And in dozens of places, we would have printf("%%FORMAT%%", format_double(val1), format_double(val2), format_double(val3), format_double(val4), format_double(val5));

    It is actually possible for this to work, but none of the modern libc implementations work that way. SCO's SysV derivative might just be old and arcane enough to have an argument-at-a-time implementation of printf/stdarg.

    Basically, the behaviour is undefined, and while glibc/gcc compiles it as executing all the format_double calls first, a different system may interleave them with the printf execution (particularly if it unwinds and inlines printf calls). It wouldn't be legal under C99, but SysV didn't even implement C89, on account of predating it by several years (IIRC, it used some bizarre extension of K&R C with prototypes).

    Obviously it's dumb to rely on this behaviour. It won't work on anything less than about 20 years old. 



  • How did you fix it? Did you change all those places where it was used incorrectly, or did you try something along the lines of
     

    char *format_double(double dbl){
    static char string[100][20];
    static int n;
     
    if (n<0 || n>=99) n=0; else n++;
     
    memset(string[n], '\0', 20);

    // Do some work to turn a number ito a formatted string like
    // 634529.04 ==> "$634,529.04"
    ... CODE FOR THAT ...

    return string[n];

    }



  • @ammoQ said:

    static char string[100][20];
    static int n;
     
    if (n<0 || n>=99) n=0; else n++;
    ...
    return string[n];

    Now you're freaking me out... Being capable of thinking about something like this is very very strange... :)
    + Threadsafety FTW!
     



  • @viraptor said:

    @ammoQ said:
    static char string[100][20];
    static int n;
     
    if (n<0 || n>=99) n=0; else n++;
    ...
    return string[n];

    Now you're freaking me out... Being capable of thinking about something like this is very very strange... :)
    + Threadsafety FTW! 

    The funny thing is, chances are that it perfectly solves the problem with an effort of 5 minutes. (Granted, if you need thread safety, it takes a bit^h^h^h lot more...)



  • @coplate said:

    char *format_double(double dbl){
     
    // 634529.04 ==> "$634,529.04"

    Almost missed it - another nice WTF - using double for currency ;) 



  • @ammoQ said:

    @viraptor said:
    @ammoQ said:

    static char string[100][20];

    static int n;

     

    if (n<0 || n>=99) n=0; else n++;

    ...

    return string[n];

    Now you're freaking me out... Being capable of thinking about something like this is very very strange... :)

    • Threadsafety FTW! 

    The funny thing is, chances are that it perfectly solves the problem with an effort of 5 minutes. (Granted, if you need thread safety, it takes a bit^h^h^h lot more...)



    Actually, because this code is part of a library that we use, I actually suggested that to him as a fix that would not rely on changing any other code on the system, while leaving the original intended functions.

    But he decided to go ahead and rewrite all the programs that use it to use a different functions that sends in a char pointer.

    something like

    int format_double(double db, char *string, int string_len){
    char temp[40];
    int new_len;
    memset(temp,'\0',40)

    // Check if double will not fit into the string
    {
    return -1;
    }
    // Do some work to turn a number ito a formatted string like
    // 634529.04 ==> "$634,529.04"
    // and stuff it into temp
    ... CODE FOR THAT ...
    strcpy(string,temp);

    return strlen(string);
    }



  • @viraptor said:

    @coplate said:

    char *format_double(double dbl){

     

    // 634529.04 ==> "$634,529.04"

    Almost missed it - another nice WTF - using double for currency ;) 



    We use doubles for currency because we don't stop at 2 decimal points, we have some things that we process at 6 decimal points.
    And then we have to convert currencies and that means floating point math.



  • @coplate said:

    @viraptor said:

    @coplate said:

    char *format_double(double dbl){

    // 634529.04 ==> "$634,529.04"

    Almost missed it - another nice WTF - using double for currency ;)



    We use doubles for currency because we don't stop at 2 decimal points, we have some things that we process at 6 decimal points.
    And then we have to convert currencies and that means floating point math.

    And presumably you don't use decimalised currency in whatever part of the world you live in. IEEE floating point values cannot represent values like 0.1, 0.01, etc.

    This is why serious financial applications don't use floating point - they use fixed-point math instead.



  • @asuffield said:

    @coplate said:

    @viraptor said:

    @coplate said:

    char *format_double(double dbl){

    // 634529.04 ==> "$634,529.04"

    Almost missed it - another nice WTF - using double for currency ;)



    We use doubles for currency because we don't stop at 2 decimal points, we have some things that we process at 6 decimal points.
    And then we have to convert currencies and that means floating point math.

    And presumably you don't use decimalised currency in whatever part of the world you live in. IEEE floating point values cannot represent values like 0.1, 0.01, etc.

    This is why serious financial applications don't use floating point - they use fixed-point math instead.



    Well, this may just be ignorance on my side then, but how does a fixed point system handle taking values form one system that are fixed at 2 decimal points and adding them to numbers from another system that is fixed at 6 decimal points, and then multiplying it with other numbers that will have 3 decimal points. 

    I just graduated last may, and we unfortuantely didnt talk about the benifits of things like packed decimals and the like. 

     I just assume that the FPU in our servers work well enough to represent all of our numbers.


  • @coplate said:

    Well, this may just be ignorance on my side then, but how does a fixed point system handle taking values form one system that are fixed at 2 decimal points and adding them to numbers from another system that is fixed at 6 decimal points, and then multiplying it with other numbers that will have 3 decimal points.
    Exactly like it does with floating, but in software. Easiest way to implement -> get biggest number of decimal digits that you need and just stick with that, (you won't need more than 4 probably), or just store tuple {number, no. of digits after point}
    @coplate said:

    I just graduated last may, and we unfortuantely didnt talk about the benifits of things like packed decimals and the like.
    I just assume that the FPU in our servers work well enough to represent all of our numbers.

    It fails with something as low as 0.1 already. You won't notice it usually. But it's like with race conditions - one day it will hit you. You'll do something like 0.2-0.1-0.1 (just example - didn't check this one exactly), and will get -0.0000001 in result. Multiply by something and you've got problem ready. If it's a bigger program you may consider real libs like GMP.



  • @coplate said:

    I just assume that the FPU in our servers work well enough to represent all of our numbers.

    They don't even come close. IEEE floating point values are designed to be a sufficient approximation for operations on a continuous scale, because this problem occurs frequently in research. They're almost useless for discrete values like money - you're expected to use integer math for that, and do your own fixed-point or infinite-precision routines (standard libraries for both are available in most major languages).

    Even with that, you cannot use IEEE floats for anything important without actually understanding how and when they work. The details are subtle and there are many provisos in order to preserve sufficient accuracy. All basic arithmetic operations on IEEE floats are neither commutative nor distributive (and they're only associative under certain circumstances), so they don't behave like the arithmetic that you're familiar with, and have to be handled more carefully.

    The only times you should ever use them are when either (a) you don't give a damn about accuracy, or (b) you really know what you're doing.
     



  • @viraptor said:

    @ammoQ said:
    static char string[100][20];
    static int n;
     
    if (n<0 || n>=99) n=0; else n++;
    ...
    return string[n];

    Now you're freaking me out... Being capable of thinking about something like this is very very strange... :)
    + Threadsafety FTW!

    LoL, I actually used the same hack to fix some broken code the other day... 

    [source]
    void postEvent( const char* type, const char* name,View* view)
    {
     //   static EventHeader evh;
        //^This is bad - what if 2 messages get sent in one frame!?
        // (i.e. before the next time the message queue is checked)
        //The struct needs to be dynamically allocated, but then who cleans it up?

        //HACK HACK - to 'avoid' the above problem i'll just cycle a list of structures for now
        static const unsigned int SIZE_OF_DIRTY_HACK = 32;//max 32 events per frame...
        static EventHeader evhs[SIZE_OF_DIRTY_HACK];
        static unsigned int currentHackIndex = 0;
        EventHeader& evh = evhs[currentHackIndex++];
        if( currentHackIndex >= SIZE_OF_DIRTY_HACK )
            currentHackIndex = 0;

        assert( type && name );
        evh.type eventStr2Id(type);
        evh.name = name;
              
        PostMessage(NULL, EVENT, WPARAM(&evh), LPARAM(view));
    }

    [/source]
     



  • On the doubles-for-currency issue, it really depends on what the requirements for accuracy and precision are.

    Most likely, what you have to do is GET IT RIGHT according to a set of rules that may seem at once fussy, harsh, and arbitrary.  In the banking industry, that would mean some rules that are based on decades of practice by bankers and accountants but will seem almost but not quite mathematically correct.

     These won't quite match up with the rules that engineers and scientists use for floating-point.

     



  • double means what now??

    Double represents things in an efficient binary manner - One person described it's format as similar to "1/2 + 0/4 + 1/8 + 1/16 +  0/32 ...." This means that it cannot accurately represent a decimal number: ie double n != int .01 for _all_ values of n. If you are calculating in double, you are calculating inaccurately.

    This is going to be biting you if you are using float to handle currency: guaranteed. You _will_ have calculations that are inaccurate by 1c. You just haven't found out about it yet.

    And as for handling systems that have differing accuracies: The science world states that you use the lowest accuracy: ie .01 + .0135 = .02 . Might not be how the financial system works. But this has been fixed by others, though, as other's stated: fixed point math, with a root to cover the most accurate system you have to deal with. Mind you, that 'infinite accuracy' math sounds usefull.
     



  • @robbak said:

    This is going to be biting you if you are using float to handle currency: guaranteed. You will have calculations that are inaccurate by 1c. You just haven't found out about it yet.

    The inaccuracies can be much larger than that. For example, for finding the difference between two floating point values in base B, the error can be anything up to (B-1) times the size of the result. For base 2 values like IEEE doubles, that means every digit of the result can be wrong (although the exponent will be correct). In the general case, if you get an answer of $1,000,000, the correct answer lies somewhere between $0 and $2,000,000.

    This is why you can only use floats for anything if you either know what you're doing or just don't care. You can control the precision in whatever specific cases you're working with, but you must understand the mathematics behind floats in order to do it, and it is not even remotely easy.

    (Multiplication and division are far worse) 

     

    And as for handling systems that have differing accuracies: The science world states that you use the lowest accuracy: ie .01 + .0135 = .02.

    That's just what they teach in schools. For real science, you compute the true precision of your result and quote it as a range. (0.1 +/- 0.05) + (0.135 +/- 0.005) == (0.235 +/- 0.055), or anything between 0.18 and 0.29. Obviously this requires an understanding of what is known as the "error propagation" of every operation in your system - you have to compute the actual impact of each primitive and function on the error. This process is quite complicated when non-trivial operations like logarithms and sines are involved.

     

    Might not be how the financial system works.

     

    It isn't. The financial system doesn't work on continuous scales like these at all, it uses discrete mathematics with narrowly defined behaviour. The exact rules vary depending on where in the world you are, but they're always something along the lines of "all values are computed to precisely three decimal places, and a + b is defined as returning results according to banker's rounding". There is no error involved, the basic arithmetic operations are just redefined to work like this. This is still a real pain to work with, because it's still neither commutative, distributive, nor associative, but it's an entirely different set of rules to the ones implemented for IEEE floats, so you can't use them.

    Note that algebra does not work properly under the financial rules, so you can't rely on it. Things that you take for granted as being true in mathematics do not necessarily apply. Example: a + b == a + c does not imply that b == c, so you cannot simplify the expression like that - it's not a problem of precision, the right answer is that b and c may be different and the result may still be true, under the financial rules. This is every bit as horrible to work with as it sounds.



  • @coplate said:

    I just assume that the FPU in our servers work well enough to represent all of our numbers.

    Don't.

    Google for "What Every Computer Scientist Should Know About Floating-Point" and read it.
     



  • @gremlin said:

    @coplate said:
    I just assume that the FPU in our servers work well enough to represent all of our numbers.

    Don't.

    Google for "What Every Computer Scientist Should Know About Floating-Point" and read it.
     

    And be aware that it is to floating point math what those "introduction to java" articles are to programming - it just shows you what the problem looks like, it doesn't leave you capable of solving complex problems with it. It was only ever intended to show enough for people to realise that they're doing it wrong, not how to do it right.


Log in to reply
 

Looks like your connection to What the Daily WTF? was lost, please wait while we try to reconnect.