The best way in C++ to cast different signedness types each other?

530 Views Asked by At

There is an uint64_t data field sent by the communication peer, it carries an order ID that I need to store into a Postgresql-11 DB that do NOT support unsigned integer types. Although a real data may exceed 2^63, I think a INT8 filed in Postgresql11 can hold it, if I do some casting carefully.

Let's say there be:

uint64_t order_id = 123; // received
int64_t  to_db;          // to be writed into db

I plan to use one of the following methods to cast an uint64_t value into an int64_t value:

  1. to_db = order_id; // directly assigning;
  2. to_db = (int64_t)order_id; //c-style casting;
  3. to_db = static_cast<int64_t>(order_id);
  4. to_db = *reinterpret_cast<const int64_t*>( &order_id );

and when I need to load it from the db, I can do a reversed casting.

I know they all work, I'm just interested in which one meet the C++ standard the most perfectly.

In other words, which method will always work in whatever 64bit platform with whatever compiler?

4

There are 4 best solutions below

0
Sneftel On

All four methods will always work, as long as the value is within range. The first will generate warnings on many compilers, so should probably not be used. The second is more a C idiom than a C++ idiom, but is widely used in C++. The last one is ugly and relies on subtle details from the standard, and should not be used.

0
Swift - Friday Pie On

Depends where it would be compiled and run... any of those not fully portable without C++20 support.

The safest way without that would be doing conversion yourself by changing range of values, something like that

int64_t to_db = (order_id > (uint64_t)LLONG_MAX) 
           ? int64_t(order_id - (uint64_t)LLONG_MAX - 1) 
           : int64_t(order_id ) - LLONG_MIN;

uint64_t from_db = (to_db < 0) 
                    ? to_db + LLONG_MIN
                    : uint64_t(to_db) +  (uint64_t)LLONG_MAX  + 1;

If order_id is greater than (2^63 -1), then order_id - (uint64_t)LLONG_MAX - 1 yields a non-negative value. If not, then cast to signed is well defined and subtraction ensures values to be shifted into negative range.

During reverse conversion, to_db + LLONG_MIN places value into [0, ULLONG_MAX] range.

and do opposite on reading. Database platform or compiler you use may do something awful with binary representation of unsigned values when converting them to signed, not to mention that different format of signed do exist.

For same reason inter-platform protocols often involve use of string formatting or "least bit's value" for representing floating point values as integers, i.e. as encoded fixed point.

2
n. m. could be an AI On

This function seems UB-free

int64_t fromUnsignedTwosComplement(uint64_t u)
{
    if (u <= std::numeric_limits<int64_t>::max()) return static_cast<int64_t>(u);
    else return -static_cast<int64_t>(-u);
}

It reduces to a no-op under optimisations.

Conversion in the other direction is a straight cast to uint64_t. It is always well-defined.

8
Daniel Langr On

I would go with memcpy. It avoids (? see comments) undefined behavior and typically compilers optimize any byte copying away:

int64_t uint64_t_to_int64_t(uint64_t u)
{
  int64_t i;
  memcpy(&i, &u, sizeof(int64_t));
  return i;
}

order_id = uint64_t_to_int64_t(to_db);

GCC with -O2 generated the optimal assembly for uint64_t_to_int64_t:

mov rax, rdi
ret

Live demo: https://godbolt.org/z/Gbvhzh