Технически невозможно реализовать memcpy с нуля в стандарте C?

Говард Чу пишет:

В последней спецификации C невозможно написать "легальную" реализацию malloc или memcpy.

Это правильно? У меня сложилось впечатление, что в прошлом цель (по крайней мере) стандарта заключалась в том, что что-то вроде этого будет работать:

void * memcpy(void * restrict destination, const void * restrict source, size_t nbytes)
{
    size_t i;
    unsigned char *dst = (unsigned char *) destination;
    const unsigned char *src = (const unsigned char *) source;

    for (i = 0; i < nbytes; i++)
        dst[i] = src[i];
    return destination;
}

Какие правила в последнем стандарте C здесь нарушены? Или какая часть спецификации memcpy неправильно реализована этим кодом?

Ответы

Ответ 1

Как и C++, C делает исключения для доступа к любой памяти через символьные типы. § 6.5:

  1. Эффективным типом объекта для доступа к его сохраненному значению является объявленный тип объекта, если таковой имеется. 87) Если значение сохраняется в объекте, у которого нет объявленного типа, через lvalue, имеющий тип, который не является символьным типом, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменить сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа, используя memcpy или memmove, или копируется как массив символьного типа [emph. мой], то эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют значение, является эффективным типом объекта, из которого копируется значение, если оно имеется. Для всех других доступов к объекту, не имеющему объявленного типа, эффективный тип объекта - это просто тип lvalue, используемого для доступа.

  2. Объект должен иметь свое сохраненное значение, доступное только через выражение lvalue, которое имеет один из следующих типов:

    • [...]
    • тип персонажа.

87) Выделенные объекты не имеют объявленного типа.

Поэтому мне кажется, что вы все еще можете писать функции mem* как C, если они работают с char*/unsigned char*, как в вашем примере.

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

Конечно, подвох в том, что платформе C разрешено делать все, что она хочет. Если ваша платформа совместима с C, то "технически не ваше дело", как выполняются требования. Это не ново: еще один пример - setjmp и longjmp не могут быть реализованы на C.

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

На практике много реализаций malloc написаны на C или слегка расширенном C. Например, Clang поддерживает __attribute__((malloc)) как расширение, которое сообщает компилятору, что результирующие объекты выполняют все значения, необходимые для обработки как выделенный объект. Однако для этого требуется, чтобы освобождающие функции были названы "свободными", "_ZdlPvm" или вариантами (и обрабатывает их особым образом). В GCC также есть __attribute__((malloc)), но я не знаю, как он работает free.

Ответ 2

TL; DR
Это должно быть хорошо, если memcpy основан на наивной символьной копии.

И не оптимизирован для перемещения кусков размером самого большого выровненного типа, которые могут быть скопированы в одной инструкции. Последнее - то, как это делают стандартные реализации lib.


Что касается что-то вроде этого сценария:

void* my_int = malloc(sizeof *my_int);
int another_int = 1;

my_memcpy(my_int, &another_int, sizeof(int));

printf("%d", *(int*)my_int); // well-defined or strict aliasing violation?

Объяснение:

  • Данные, указанные в my_int не имеют эффективного типа.
  • Когда мы копируем данные в расположение my_int, может возникнуть my_int тем, что мы заставляем эффективный тип стать unsigned char, поскольку это то, что использует my_memcpy.
  • А потом, когда мы читаем эту ячейку памяти через int*. Будем ли мы нарушать строгий псевдоним?

Однако ключ здесь является специальным исключением в правиле для эффективного типа, указанного в C17 6.5/6, выделено мной:

Если значение копируется в объект, не имеющий объявленного типа, с использованием memcpy или memmove, или копируется как массив символьного типа, тогда эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют значение, является действующий тип объекта, из которого копируется значение, если оно есть.

Поскольку мы копируем массив как символьный тип, эффективный тип того, на что указывает my_int, станет типом объекта another_int из которого было скопировано значение.

Так что все должно быть хорошо.

Кроме того, вы restrict -qualified параметры, чтобы не было суеты в отношении того, могут ли два указателя совмещать друг друга, как настоящий memcpy.

Примечательно, что это правило не изменилось до C99, C11 и C17. Кто-то может поспорить, что это очень плохое правило, которым злоупотребляют производители компиляторов, но это другая история.

Ответ 3

Для функции malloc, параграф 6.5 §6 проясняет, что невозможно написать совместимую и переносимую реализацию C:

Эффективным типом объекта для доступа к его сохраненному значению является объявленный тип объекта, если таковой имеется (87)...

(Ненормативная) записка 87 гласит:

Выделенные объекты не имеют объявленного типа.

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

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

Но оставшаяся часть этого же абзаца объясняет, что нет реальной проблемы в написании реализации memcpy (подчеркните мою):

... Если значение сохраняется в объекте, у которого нет объявленного типа, через lvalue, имеющий тип, который не является символьным типом, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих обращений, которые делают не изменять сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа, с использованием memcpy или memmove, или копируется как массив символьного типа, тогда эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют значение, является действующий тип объекта, из которого копируется значение, если оно есть. Для всех других доступов к объекту, не имеющему объявленного типа, эффективный тип объекта - это просто тип lvalue, используемого для доступа.

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

ИМХО, напыщенная речь Говарда Чу о том старом добром использовании memcpy, которое больше не действует (при условии, что sizeof(float) == sizeof(int)):

float f = 1.0;
int i;
memcpy(&i, &f, sizeof(int));         // valid: copy at byte level, but the value of i is undefined
print("Repr of %f is %x\n", i, i);   // UB: i cannot be accessed as a float