Re: Type-punning / casting problem
On Sep 14, 9:02 pm, Phil Endecott <spam_from_usenet_0...@chezphil.org>
wrote:
I need a function that takes a float, swaps its endianness (htonl) in
place, and returns a char* pointer to its first byte. This is one of a
family of functions that prepare different data types for passing to
another process.
I have got confused by the rules about what won't work, what will work,
and what might work, when casting. Specifically, I have an
implementation that works until I remove my debugging, at which point
the compiler seems to decide that it can optimise away the writes to the
bytes other than the first, or something like that. Here it is:
template <typename T>
inline const char* encode_arg(T& t); // linker error if you try to
// encode a type for which there
// is no implementation
// This one works:
template <>
inline const char* encode_pq_arg<int>(int& i) {
i = htonl(i);
return reinterpret_cast<const char*>(&i);
}
// This one doesn't:
template <>
inline const char* encode_arg<float>(float& f) {
uint32_t* ptr = reinterpret_cast<uint32_t*>(&f);
*ptr = htonl(*ptr);
What type does htonl take. And what does it do with it. This
looks like undefined behavior to me.
const char* cptr = reinterpret_cast<const char*>(ptr);
return cptr;
}
When I dump cptr[0] to cptr[3] before the return, it works. Without the
debug, it fails; it's obviously hard to see what ends up in the result
in that case, but it looks undefined.
So, is there some tweak that will make this work, and be certain to
work? Am I better off using a union, or is that even less defined?
Even less defined.
Does anyone know what the rules actually are?
The rules are that if the variable is declared as a float, the
only way you can access it is as a float, or as an array of char
or unsigned char. Anything else is unsigned behavior.
In practice, most compilers allow more; I think that g++
explicitly guarantees type punning with a union, and not a few
compilers guarantee it when a cast is used. Still, if you
really want to use htonl, the "correct" solution is to memcpy
the bytes of the float into a uint32_t, and call the function on
that. (Technically, even that can fail, but in practice, you're
probably safe.)
The "correct" way to stream a float, of course, is to extract
the exponent, sign and mantissa using functions like frexp and
ldexp. I've experimented with doing so; it's less complex than
it sounds, and at least on a Sun Sparc under Solaris, it's not
outrageously expensive in runtime either (which, I'll admit,
surprised me). My current code for writing a float, for
example, looks something like:
bool isNeg = source < 0 ;
if ( isNeg ) {
source = - source ;
}
int exp ;
if ( source == 0.0 ) {
exp = 0 ;
} else {
source = ldexp( frexp( source, &exp ), 24 ) ;
exp += 126 ;
}
unsigned long mant = source ;
dest.put( (isNeg ? 0x80 : 0x00) | exp >> 1 ) ;
dest.put( ((exp << 7) & 0x80) | ((mant >> 16) & 0x7F) ) ;
dest.put( mant >> 8 ) ;
dest.put( mant ) ;
Note that this code is independent of the host byte order, or
even it's floating point representation. It will always output
an IEEE float, high byte first, regardless of what the local
hardware does.
--
James Kanze (GABI Software) email:james.kanze@gmail.com
Conseils en informatique orient=E9e objet/
Beratung in objektorientierter Datenverarbeitung
9 place S=E9mard, 78210 St.-Cyr-l'=C9cole, France, +33 (0)1 30 23 00 34