Лучший способ в C использовать разные типы подписей друг для друга?

#c #casting #reinterpret-cast #static-cast #signedness

#c #Кастинг #переинтерпретировать-приведение #статическое приведение #подписанность

Вопрос:

Существует поле данных uint64_t, отправляемое одноранговым узлом связи, оно содержит идентификатор заказа, который мне нужно сохранить в базе данных Postgresql-11, которые НЕ поддерживают целочисленные типы без знака. Хотя реальные данные могут превышать 2 ^ 63, я думаю INT8 , что файл в Postgresql11 может содержать его, если я тщательно выполню кастинг.

Допустим, существует:

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

Я планирую использовать один из следующих методов для преобразования значения uint64_t в значение int64_t:

  1. to_db = order_id; // прямое назначение;
  2. to_db = (int64_t)order_id; // приведение в стиле c;
  3. to_db = static_cast<int64_t>(order_id);
  4. to_db = *reinterpret_cast<const int64_t*>( amp;order_id );

и когда мне нужно загрузить его из базы данных, я могу выполнить обратное приведение.

Я знаю, что все они работают, мне просто интересно, какой из них соответствует стандарту C наиболее идеально.

Другими словами, какой метод всегда будет работать на любой 64-битной платформе с любым компилятором?

Спасибо!!!

Комментарии:

1. Каждое из них является неопределенным поведением, если значение превышает 2 ^ 63-1.

2. Существует дополнительная опция: memcpy(amp;to_db, amp;order_id, 8); .

3. @DanielLangr это работает .. до тех пор, пока система, которая записала эти значения, и система, которая их считывает, находятся в одинаковых условиях

4. @n.’местоимения’m. Более того, AFAIK, 4-й случай всегда приводит к неопределенному поведению.

Ответ №1:

Зависит от того, где он будет скомпилирован и запущен… любой из них не является полностью переносимым без поддержки C 20.

Самый безопасный способ без этого — выполнить преобразование самостоятельно, изменив диапазон значений, что-то вроде этого

 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;
 

Если order_id больше (2 ^ 63 -1), то order_id - (uint64_t)LLONG_MAX - 1 дает неотрицательное значение. Если нет, то приведение к знаку четко определено, и вычитание гарантирует, что значения будут сдвинуты в отрицательный диапазон.

Во время обратного преобразования to_db LLONG_MIN помещает значение в диапазон [0, ULLONG_MAX].

и делайте обратное при чтении. Используемая вами платформа базы данных или компилятор могут сделать что-то ужасное с двоичным представлением значений без знака при преобразовании их в signed, не говоря уже о том, что существуют разные форматы signed .

По той же причине межплатформенные протоколы часто предполагают использование строкового форматирования или «значения наименьшего бита» для представления значений с плавающей запятой в виде целых чисел, то есть в виде закодированной фиксированной точки.

Ответ №2:

Я бы пошел с memcpy . Это позволяет избежать (? см. Комментарии) неопределенного поведения, и обычно компиляторы оптимизируют любое копирование байтов:

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

order_id = uint64_t_to_int64_t(to_db);
 

GCC -O2 сгенерировал оптимальную сборку для uint64_t_to_int64_t :

 mov rax, rdi
ret
 

Живая демонстрация: https://godbolt.org/z/Gbvhzh

Комментарии:

1. «Это позволяет избежать неопределенного поведения» Я бы не стал делать такое заявление.

2. @n.’местоимения’m. Почему бы и нет?

3. memcpy само по себе все в порядке, проблематичным является последующее использование значения. C 20 может сделать это нормально, не уверен в этом.

4. @n.’местоимения’m. Не могли бы вы объяснить, пожалуйста?

5. Я не думаю, что вы можете поместить любой случайный битовый шаблон в целочисленный тип со знаком и ожидать, что он сработает. Я не вижу такой гарантии в стандарте.

Ответ №3:

Все четыре метода будут работать всегда, пока значение находится в пределах диапазона. Первый будет генерировать предупреждения на многих компиляторах, поэтому, вероятно, его не следует использовать. Второй вариант скорее является идиомой C, чем идиомой C , но широко используется в C . Последний уродлив и зависит от тонких деталей стандарта, и его не следует использовать.

Ответ №4:

Эта функция кажется свободной от UB

 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);
}
 

Это сводится к отказу от оптимизации.

Преобразование в другом направлении — это прямое приведение к uint64_t . Это всегда четко определено.

Комментарии:

1. Не могли бы вы подробнее рассказать о том, что -u происходит в последнем приведении?

2. @Surt Он вычисляет выражение -u по модулю 2^64 , что было бы таким же , как 2^64-u если бы мы могли записать константу 2^64 .