Делает ли std :: memcpy свое назначение определенным? - PullRequest
9 голосов
/ 02 июля 2019

Вот код:

unsigned int a;            // a is indeterminate
unsigned long long b = 1;  // b is initialized to 1
std::memcpy(&a, &b, sizeof(unsigned int));
unsigned int c = a;        // Is this not undefined behavior? (Implementation-defined behavior?)

Гарантируется ли стандартом a, что это определенное значение, когда мы получаем к нему доступ для инициализации c? Cppreference говорит :

void* memcpy( void* dest, const void* src, std::size_t count );

Копирует count байт с объекта, на который указывает src, на объект, на который указывает dest. Оба объекта интерпретируются как массивы unsigned char.

Но я не вижу нигде в cppreference, где говорится, что если неопределенное значение «скопировано» таким образом, оно становится детерминированным.

Из стандарта кажется, что это аналогично этому:

unsigned int a;            // a is indeterminate
unsigned long long b = 1;  // b is initialized to 1
auto* a_ptr = reinterpret_cast<unsigned char*>(&a);
auto* b_ptr = reinterpret_cast<unsigned char*>(&b);
a_ptr[0] = b_ptr[0];
a_ptr[1] = b_ptr[1];
a_ptr[2] = b_ptr[2];
a_ptr[3] = b_ptr[3];
unsigned int c = a;        // Is this undefined behavior? (Implementation defined behavior?)

Похоже, что стандарт оставляет место для этого, поскольку правила псевдонимов типов допускают доступ к объекту a как unsigned char таким образом. Но я не могу найти то, что говорит, что a больше не является неопределенным.

Ответы [ 3 ]

4 голосов
/ 02 июля 2019

Разве это не неопределенное поведение

Это UB, потому что вы копируете в неправильный тип. [basic.types] 2 и 3 разрешают копирование байтов, но только между объектами одного типа. Вы скопировали из long long в int. Это не имеет ничего общего с неопределенностью значения. Даже если вы копируете только sizeof(int) байт, тот факт, что вы не копируете с фактического int, означает, что вы не получаете защиту этих правил.

Если вы копировали в значение того же типа, то [basic.types] 3 говорит, что это эквивалентно их простому назначению. То есть a "впоследствии будет иметь то же значение, что и" b.

1 голос
/ 02 июля 2019

TL; DR: определяется реализацией, будет ли неопределенное поведение или нет.Пробный стиль с номерами строк кода:


  1. unsigned int a;

Предполагается, что переменная a имеет длительность автоматического хранения.Его время жизни начинается (6.6.3 / 1).Поскольку это не класс, его время жизни начинается с инициализации по умолчанию, при которой никакая другая инициализация не выполняется (9.3 / 7.3).

unsigned long long b = 1ull;

Предполагается, что переменная b имеет длительность автоматического хранения.Его время жизни начинается (6.6.3 / 1).Поскольку это не класс, его время жизни начинается с инициализации копирования (9.3 / 15).

std::memcpy(&a, &b, sizeof(unsigned int));

Согласно 16.2 / 2, std::memcpy должен иметь ту же семантику и предварительные условия, что и стандартная библиотека C * memcpy.В стандарте C 7.21.2.1, предполагая, что sizeof(unsigned int) == 4, 4 символа копируются из объекта, на который указывает &b, в объект, на который указывает &a.(Эти два пункта - то, чего не хватает в других ответах.)

На этом этапе размеры unsigned int, unsigned long long, их представления (например, порядковый номер) и размер символа являются реализацией.определено (насколько я понимаю, см. 6.7.1 / 4 и его примечание о том, что применяется ISO C 5.2.4.2.1).Я предполагаю, что реализация имеет младший порядок, unsigned int - это 32 бита, unsigned long long - это 64 бита, а символ - 8 бит.

Теперь, когда я сказал, что такое реализация, я знаю,что a имеет значение-представление для unsigned int, равного 1u.Пока что ничто не было неопределенным поведением.

unsigned int c = a;

Теперь у нас есть доступ a.Затем в 6.7 / 4 говорится, что

Для тривиально копируемых типов представление значения представляет собой набор битов в представлении объекта, который определяет значение, которое является одним дискретным элементом определенного реализацией набораvalues.

Теперь я знаю, что значение a определяется битами значений, определяемыми реализацией в a, которые, как я знаю, содержат представление-значение для 1u.Тогда значение a равно 1u.

Затем, подобно (2), переменная c инициализируется копией до 1u.


Мы использовали реализацию, определяемую реализациейзначения, чтобы найти то, что происходит.Возможно, что значение, определенное реализацией 1ull, равно , а не одно из набора значений, определенного реализацией, для unsigned int.В этом случае доступ к a будет неопределенным поведением, потому что стандарт не говорит, что происходит, когда вы обращаетесь к переменной с недопустимым представлением значения.

AFAIK, мы можем воспользоваться преимуществамиДело в том, что большинство реализаций определяют unsigned int, где любая возможная битовая комбинация является допустимым представлением значения.Следовательно, не будет неопределенного поведения.

0 голосов
/ 02 июля 2019

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

Я полагаю, что в некоторых случаях это поведение, определяемое реализацией, а в других - неопределенное (так как другой ответ пришел к выводу по аналогичным причинам).В некотором смысле это реализация, определенная, если это неопределенное поведение, или определенная реализация, поэтому я не уверен, что она вообще не определена, имеет приоритет в такой классификации.

, поскольку std::memcpy полностью работает на объектном представлениирассматриваемые типы (путем совмещения имен указателей, присвоенных unsigned char, как указано в 6.10 / 8.8 [basic.lval]).Если биты в рассматриваемых байтах unsigned long long гарантированно являются чем-то конкретным, вы можете манипулировать ими по своему усмотрению или записывать их в объектное представление любого другого типа.Тип назначения будет затем использовать биты для формирования своего значения на основе его представления значения (каким бы оно ни было), как определено в 6.9 / 4 [basic.types]:

Представление объектаОбъект типа T - это последовательность из N беззнаковых объектов char, занятых объектом типа T, где N равно sizeof (T).Представление значения объекта представляет собой набор битов, которые содержат значение типа T. Для тривиально копируемых типов представление значения представляет собой набор битов в представлении объекта, которое определяет значение, которое является одним дискретным элементом реализации.определенный набор значений.

И это:

Предполагается, что модель памяти C ++ совместима с моделью языка программирования ISO / IEC 9899 C.

Зная это, все, что сейчас имеет значение, - это то, каково объектное представление рассматриваемых целочисленных типов.Согласно 6.9.1 / 7 [basic.fundemental]:

Типы bool, char, char16_t, char32_t, wchar_t, а также целочисленные типы со знаком и без знака называются целыми типами.Синоним для целочисленного типа - целочисленный тип.Представления целочисленных типов должны определять значения с использованием чисто двоичной системы счисления.[Пример: этот международный стандарт разрешает два дополнения, одно дополнение и представление величины со знаком для целочисленных типов.- конец примера]

Однако в сноске разъясняется определение "двоичной системы нумерации":

Позиционное представление для целых чисел, использующее двоичные цифры 0 и 1,в котором значения, представленные последовательными битами, являются аддитивными, начинаются с 1 и умножаются на последовательную интегральную степень 2, за исключением, возможно, бита с самой высокой позицией.(Адаптировано из Американского национального словаря по системам обработки информации.)

Мы также знаем, что целые числа без знака имеют то же представление значений, что и целые числа со знаком, только под модулем в соответствии с 6.9.1 / 4 [basic.fundemental]:

Беззнаковые целые должны подчиняться законам арифметики по модулю 2 ^ n, где n - количество бит в представлении значения этого конкретного размера целого числа.

Хотя это не говорит точно, каким может быть представление значения, на основе указанного определения двоичной системы нумерации, последовательные биты должны быть аддитивными степенями двух, как и ожидалось (вместо того, чтобы позволять битам быть в любом заданном порядке), за исключением, возможно, настоящего знака бита.Кроме того, поскольку представления значений со знаком и без знака, это означает, что целое число без знака будет храниться как возрастающая двоичная последовательность вплоть до 2 ^ (n-1) (в прошлом, в зависимости от того, как обрабатываются числа со знаком, вещи определяются реализацией).

Однако есть еще некоторые другие соображения, такие как порядковый номер байтов и количество битов заполнения, которые могут присутствовать из-за того, что sizeof(T) измеряет только размер представления объекта, а не представление значения (как указано ранее). Поскольку в C ++ 17 не существует стандартного способа (я думаю) проверки на порядковый номер, это является основным фактором, который оставил бы эту реализацию в зависимости от того, каким будет результат. Что касается битов заполнения, хотя они могут присутствовать (но не указано, где они будут, что я могу сказать, кроме того, что они не будут прерывать непрерывную последовательность битов, образующих представление значения целого числа), запись в них может оказаться потенциально проблематичным. Поскольку цель модели памяти C ++ основана на модели памяти стандарта C99 «сопоставимым» способом, сноска из 6.2.6.2 (на которую в стандарте C ++ 20 ссылаются как на примечание, чтобы напомнить, что она основана на том, что ) можно сказать следующее:

Некоторые комбинации битов заполнения могут генерировать представления ловушек, например, если один бит дополнения является битом четности. Независимо от того, нет арифметическая операция над допустимыми значениями может генерировать ловушку представление, кроме как как часть исключительного условия, такого как переполнение, и это не может произойти с неподписанными типами. Все остальные комбинации битов заполнения являются альтернативными объектными представлениями значение, указанное битами значения.

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

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

Хотя это невозможно в C ++ 17, в C ++ 20 можно использовать std::endian в сочетании с std::has_unique_object_representations<T> (который присутствовал в C ++ 17) или некоторую математику с CHAR_BIT, UINT_MAX / ULLONG_MAX и sizeof этих типов для обеспечения правильности ожидаемого порядка байтов, а также отсутствия битов заполнения, что позволяет на самом деле получить ожидаемый результат определенным образом, учитывая то, что ранее было установлено с тем, как говорят целые числа храниться Конечно, C ++ 20 также дополнительно уточняет это и указывает, что целое число должно храниться только в двух дополнениях, что устраняет дальнейшие проблемы, связанные с реализацией.

...