Лучший способ в С++ приводить разные типы подписи друг к другу?

Существует поле данных 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*>( &order_id );

и когда мне нужно загрузить его из БД, я могу сделать обратное литье.

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

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

Спасибо!!!


person Leon    schedule 02.12.2020    source источник
comment
Каждый из них является неопределенным поведением, если значение превышает 2 ^ 63-1.   -  person n. 1.8e9-where's-my-share m.    schedule 02.12.2020
comment
Есть дополнительная опция: memcpy(&to_db, &order_id, 8);.   -  person Daniel Langr    schedule 02.12.2020
comment
@DanielLangr, который работает ... пока система, которая записывает эти значения, и система, которая их читает, находятся в одинаковых условиях   -  person Swift - Friday Pie    schedule 02.12.2020
comment
@n.'местоимения'm. Более того, AFAIK, 4-й случай всегда дает неопределенное поведение.   -  person Daniel Langr    schedule 02.12.2020


Ответы (4)


Зависит от того, где он будет скомпилирован и запущен... любой из тех, которые не полностью переносимы без поддержки С++ 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].

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

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

person Swift - Friday Pie    schedule 02.12.2020

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

person Sneftel    schedule 02.12.2020

Эта функция кажется свободной от 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. Он всегда хорошо определен.

person n. 1.8e9-where's-my-share m.    schedule 02.12.2020
comment
Не могли бы вы уточнить, что -u делает в последнем составе? - person Surt; 02.12.2020
comment
@Surt Он вычисляет выражение -u по модулю 2^64, которое было бы таким же, как 2^64-u, если бы мы могли записать константу 2^64. - person n. 1.8e9-where's-my-share m.; 02.12.2020

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

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 с -O2 сгенерировал оптимальную сборку для uint64_t_to_int64_t:

mov rax, rdi
ret

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

person Daniel Langr    schedule 02.12.2020
comment
Это позволяет избежать неопределенного поведения. Я бы не стал утверждать такое. - person n. 1.8e9-where's-my-share m.; 02.12.2020
comment
@n.'местоимения'm. Почему нет? - person Daniel Langr; 02.12.2020
comment
memcpy сам по себе в порядке, проблематично последующее использование значения. С++ 20 может сделать это нормально, не уверен в этом. - person n. 1.8e9-where's-my-share m.; 02.12.2020
comment
@n.'местоимения'm. Не могли бы вы объяснить? - person Daniel Langr; 02.12.2020
comment
Я не думаю, что вы можете поместить любой случайный битовый шаблон в целочисленный тип со знаком и ожидать, что он сработает. Я не вижу такой гарантии в стандарте. - person n. 1.8e9-where's-my-share m.; 02.12.2020
comment
@n.'местоимения'm. Я знаю, что cppreference не является нормативным, но там как раз такой случай: en .cppreference.com/w/cpp/string/byte/memcpy. Нет примера с double и int64_t. Также написано: Если строгий алиасинг запрещает рассматривать одну и ту же память как значения двух разных типов, для преобразования значений может использоваться std::memcpy. - person Daniel Langr; 02.12.2020
comment
@n.'местоимения'm. Это кажется актуальным: stackoverflow.com/q/51300626/580083. Вы правы в том, что это не простая проблема, и люди спорят о том, УБ это или нет. Первые два ответа, получившие наибольшее количество голосов, расходятся во мнении :-o. Другой актуальный вопрос: stackoverflow.com/q/39595103/580083. - person Daniel Langr; 02.12.2020
comment
Оба связанных вопроса касаются беззнаковых типов. Беззнаковые типы не могут иметь представления ловушки, все битовые шаблоны представляют значения, поэтому memcpy что-либо для беззнакового типа и проверка результата прекрасно определены. Нет такой гарантии w.r.t. подписанные типы. Cppreference также должен был использовать беззнаковый тип. - person n. 1.8e9-where's-my-share m.; 02.12.2020