Today I’m learning about numeric formatting in conjunction with cout. In other, more sensible languages, you use functions or methods to format numbers.
In pre-OO languages, you usually have a format function. For example…
Format( x, "#.##")
Object orientated languages often offer formatting as a method on the number itself.
x.ToString("#.##")
In C++, which could support both styles, choose something else entirely.
cout << setprecision(3) << x;
Instead of creating a string from x like the other examples, setpercision modifies the internal state of the stream. Since cout is a globally accessible stream, this is a very bad thing.
In order to use cout safely, you need to always call setpercision immediately before passing it any floating point value. The reason for this is that you have no other way to know what the precision currently is. Any function call, even one that doesn’t write to the stream, could potentially change the precision.
This function has a subtle flaw, it doesn’t restore the precision for the stream. Since using cout for debugging is a common practice, this error can occur in any function. And it can be introduced at any time long after the original code is tested and released.
float addValues(float a, float b) { cout << setprecision (5); #ifdef _DEBUG cout << "A: " << a << endl; cout << "B: " << b << endl; #endif return (a+b); }
There is a way to safely use setprecision, but it requires some book keeping.
float addValues2(float a, float b) { #ifdef _DEBUG streamsize original = cout.precision(); cout << setprecision(4); cout << "A: " << a << endl; cout << "B: " << b << endl; cout << setprecision(original); #endif return (a+b); }
Another problem with set precision is that it isn’t a general solution. You can only use it when dealing with streams. If you want to use the value somewhere else, say in a GUI window or a database field, you have to find another technique. Compare this to the earlier examples, which could be used wherever a string is valid.
Now lets take a look at the definition for setprecision. The first thing I look at is the return type. According to the MS docs, “Return Value: The manipulator returns an object that, when extracted from or inserted into the stream str, calls str.precision(_Prec), and then returns str.”
Well that doesn’t really tell me anything. Maybe a web search will be better. The web site CPlusPlus.Com says…
“Return Value. Unspecified. This function should only be used as a stream manipulator.”
Yea, that’s real helpful. Maybe there is something of use in the header file.
_MRTIMP2 _Smanip __cdecl setprecision(streamsize);
To the novice (i.e. me), it looks like this function has three return types. Obviously this cannot be possible, so let’s examine them one at a time to try to figure out what it happening.
First off is _MRTIMP2. Well that identifier sure doesn’t give any clues as to what it means, but maybe we can find more info if we check its definition.
#define _MRTIMP2
Well who would have guessed that you can define a token as nothing? Gee, that seems really useful. Maybe I should decorate some other code with _MRTIMP2 just to confuse anyone who might steal my source code. Next up is _Smanip. Just like its counter-part _MRTIMP2, the name tells me absolutely nothing. Of course the header file is full of useful information like _Manarg is the argument value.
// TEMPLATE STRUCT _Smanip template struct _Smanip { // store function pointer and argument value _Smanip(void (__cdecl *_Left)(ios_base&, _Arg), _Arg _Val) : _Pfun(_Left), _Manarg(_Val) { // construct from function pointer and argument value }
void (__cdecl *_Pfun)(ios_base&, _Arg); // the function pointer _Arg _Manarg; // the argument value };
A look at the index for _Smanip doesn’t turn up anything direct, though one page does mention using it to create custom stream manipulators. Maybe that stands for “Stream MANIPulator”.
Well at least it is a type, which just leaves us with one mystery, __cdecl.
Turns out this is a Microsoft-specific keyword. The docs say “This is the default calling convention for C and C++ programs.”. Well, if it is the default, then why was it explicitly specified?
Now that the return type is known, we can look at the arguments. In this case there is only one, and it is of type streamsize. Fortunately this one is easy to figure out from the header.
#ifdef _WIN64 typedef __int64 streamsize; #else /* _WIN64 */ typedef int streamsize; #endif /* _WIN64 */
Now that seems like an odd choice to me. According to Wikipedia, a double precision floating point number has about 16 decimal places. That means even a single byte is more than sufficient to store this information. So why it is potentially stored as a 64-bit number?
|
I have been thinking about this a lot, and I have come to conclusion that the benefit of OOP is that it combines the interface and the implementation. When done correctly, I only have one place to look for both. When done incorrectly (C++), I have my interface and implementation scattered across several files.
Of course, this line of thinking implies interface-heavy frameworks like J2EE are dead wrong. Perhaps that is why I find them so tedious and difficult to use.