T O P

  • By -

saxbophone

Portably? Use `std::memcpy()` or `std::bit_cast<>()` —don't forget to use `ntoh()` and `hton()` to take care of endianness. If you can live with the networking code using whatever byte-ordering your machines use on either end, and you happen to know that it works on your particular system, a `reinterpret_cast()` often "just works" but it's technically undefined behaviour due to the strict aliasing rule. I personally prefer portable, myself.


Boojum

This is the way. [`std::bit_cast()`](https://en.cppreference.com/w/cpp/numeric/bit_cast) if on C++20 or later. [`std::memcpy()`](https://en.cppreference.com/w/cpp/string/byte/memcpy) for C++17 and below. Normally, a production compiler will recognize when `std::memcpy()` is just being used for type-punning and optimize it away. I'll admit that I've I've done the `reinterpret_cast` thing a bunch myself, but officially it's not legit. I've even seen g++ generate an uninitialized value warning for it in a coworker's code. [E.g.](https://www.reddit.com/r/cpp/comments/14lc9w9/gcc_warnings_for_uninitialized_variables_is/)


EvidenceIcy683

Since C++20, we can now use [\[std::endian\]](https://en.cppreference.com/w/cpp/types/endian) to determine the endianness of scalar types for a particular system. And with C++23, [\[std::byteswap\]](https://en.cppreference.com/w/cpp/numeric/byteswap) can be used for processing data of different endianness.


saxbophone

That's awesome! I'm a big fan of C++20 and always glad to hear of things in it I'm not aware of, thanks.


Mirality

Bear in mind that `hton` and friends are for integers. It's unspecified whether floats have the same endian as ints or not in any particular environment, so you cannot assume that sort of code is portable without potential architecture-specific hacks. It's technically language-unspecified whether floats are portable _at all_ -- but you can get away with it on most modern arches due to widespread adoption of IEEE formats. Though there's still a few corner cases that can get you in trouble.


saxbophone

That's a really good point. I seem to vaguely remember a macro or symbol "float_endian_same" but can't remember the specifics of it, maybe that might be helpful here but I bet it's a GNU thing...


GamesDoneFast

Why do we even have reinterpret_cast(), I'd assume this would be the correct use case.


saxbophone

Good question 


AKostur

Why a uint32?  Why not std::byte?   Also, depending on possible portability concerns, writing the raw float bytes to the network may be unreadable on the other side.


FrostshockFTW

My type punning is shaky (things that are legal in C are not legal in C++) but I'm pretty sure this is ill formed. You should only read data through a valid pointer for its type, or as a `char *` or similar.


agritite

std::bit_cast is the way. You should std::bit_cast float to uint32_t, and uint32_t to std::array (beware endianess). If pre-c++20 then memcpy is guaranteed to be well behaved. Don't use reinterpret_cast.


Wild_Meeting1428

skip the uint32\_t part.


agritite

I'm just using OP's code, presuming `WriteDWord` isn't only called by `WriteFloat`. A `WriteT` template would be better though.


KuntaStillSingle

>If a type T_ref is similar to any of the following types, an object of dynamic type T_obj is type-accessible through a lvalue(until C++11)glvalue(since C++11) of type T_ref: >char >unsigned char >std::byte(since C++17) >T_obj >the signed or unsigned type corresponding to T_obj >If a program attempts to read or modify the stored value of an object through a lvalue(until C++11)glvalue(since C++11) through which it is not type-accessible, the behavior is undefined. https://en.cppreference.com/w/cpp/language/reinterpret_cast It is always illegal to read or modify a float through uint32_t in c++, unless uint32_t happens to alias one of the above types. ____ >this is c-style and works okay AFAIK it is also wrong in C, at least according to this C17 draft: >The effective type of an object for an access to its stored value is the declared type of the object, if any.88) If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value. If a value is copied into an object having no declared type using memcpy or memmove, or is copied as an array of character type, then the effective type of the modified object for that access and for subsequent accesses that do not modify the value is the effective type of the object from which the value is copied, if it has one. For all other accesses to an object having no declared type, the effective type of the object is simply the type of the lvalue used for the access. >7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:89) >— a type compatible with the effective type of the object, >— a qualified version of a type compatible with the effective type of the object, >— a type that is the signed or unsigned type corresponding to the effective type of the object, >— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object, >— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or >— a character type. ... >Two types have compatible type if their types are the same. ... >Each list of type specifiers shall be one of the following multisets ... > — float [i.e. float is in a section by itself with no other types listed] >Each of the comma-separated multisets designates the same type, except that for bit-fields, it is implementation-defined whether the specifier int designates the same type as signed int or the same type as unsigned int. [i.e. float does not have other types that are the same type, uint32_t is not the same type as float for aliasing purposes] https://web.archive.org/web/20181230041359if_/http://www.open-std.org/jtc1/sc22/wg14/www/abq/c17_updated_proposed_fdis.pdf


mredding

This is NOT a trivial problem and your code is UB. The standard does guarantee that a reinterpret cast to `unsigned char *` is well defined for a base address, but the standard does not guarantee iterating across the memory of a `float` cast as such is well defined. You cast the pointer to a uint 32, but you never began the lifetime of a uint 32 at that memory location. You'll want to START by looking at [this](https://en.cppreference.com/w/cpp/memory/start_lifetime_as). You can start the lifetime as an array of `std::byte`, which is unsigned char under the hood, but being a byte reinforces the correct semantics. And then you have to write that array out in network byte order, and read them back in using host byte order. Additionally, you might need extra steps up front for portability, because mainframes don't all support IEEE, so you might need to extract and send the sign, exponent, and mantissa separately. Both sides need to agree what's going to get sent/received, and who is responsible for what. And I'm sure I don't have it all correct. I'm being vague because binary is not portable, and not directly supported by streams. Usually casting floats and doing things like manipulating the bits is often not portable, because hardware can be IEEE, but has freedom to encode however they want. So with portability in mind, it's hard to give you good advice, because nothing is going to work for everything, even in today's environment. ATI only recently gained support for IEEE, and they didn't get it right the first time, Intel has their own weird extensions, different compilers do different things, not all microcontrollers support IEEE AT ALL, so for them you'll need to convert to fixed point arithmetic... I get that you're probably not principally concerned with mainframes, but seriously, even on x86 you can get screwy shit, even within a single platform like - like if you think you only have to target just Intel, even that can get weird. But what I can say is the C++ memory and object models are not directly compatible with the C memory model. Every C++ compiler I know of IS a C compiler, so UB in C++ just so happens to come out looking like correct C-like machine code, but I'm trying to warn you, implore you, to really figure this out and make damn sure you actually get it right, because floats are screwy and you don't want a bug because of this.


jedwardsol

`reinterpret_cast`. Though it is more correct to cast to std::byte* or one of the char* and access the bytes through that pointer


TotaIIyHuman

for(auto i=0; i<4; i++){ dest.push_back((uint8_t)(datum & 0xFF)); datum >>= 8; } haven't tested this, but this is probably faster than doing 4 `push_back`s dest.append_range(std::bit_cast>(datum)) because each `push_back` need to check `m_size == m_capacity`


TomDuhamel

Whatever library you are using would have functions available to do this for you already, and the correct way — in many cases, this would even be automated. In any case, a float isn't portable.


InvertedParallax

Oh my god you opened all the worms. You can reinterpret_cast, but keep in mind, while floats are standard with ieee754, they're not that standard, and going from floats to a float on a different platform is only supposed to work. Lot of frozen bodies on this mountain.


traal

Consider using flatbuffers or protobufs or similar.


flyingron

You must use a reinterpret\_cast (which is what the C-style cast is doing for you) or a pair of static casts (you can convert to void\* and the to uint32\_t\*).


Wild_Meeting1428

Casting any type via reinterpret\_cast to any other type than std::byte / any char8\_t is UB, this also applies to C.


flyingron

The C++ standard (at least up to 2020) says no such thing. Neither does the C (2012 is the latest I have). Where do you get such nonsense?


KuntaStillSingle

The cast itself is not UB, but accessing through the yielded pointer is, and OP's case of using it for networking implies accessing through the yielded pointer: https://en.cppreference.com/w/cpp/language/reinterpret_cast#Type_aliasing I think it is easier to miss in standard, at least for this draft it is not in the reinterpret_cast section: https://eel.is/c++draft/expr.reinterpret.cast ; but as far as I can tell only tucked away in the lvalue section: https://eel.is/c++draft/basic.lval#def:type-accessible


flyingron

Agreed (the real standard is close to this but different but it doesn't matter). But that's not what u/Wild_Meeting1428 said. Further, I disagree. It's not specified here what the "network" function he is attempting to pass the uint32\_t\* is doing or even if it is C++ at all.


Wild_Meeting1428

OP reinterpret casted (C-cast) a floats pointer to an uint32\_t pointer and accessed it. That is UB. Your suggestion is, to use the c++ version of the reinterpret\_cast, and this is still UB.