Гарантировано ли безопасное выполнение memcpy (0,0,0)?

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

Я хотел бы знать, гарантировано ли стандартом, что memcpy(0,0,0) безопасно.

Единственное ограничение, которое я смог найти, это то, что если области памяти перекрываются, то поведение undefined...

Но можем ли мы считать, что области памяти перекрываются здесь?

Ответ 1

У меня есть проект версии стандарта C (ISO/IEC 9899: 1999), и у него есть интересные вещи, чтобы сказать об этом звонке. Во-первых, он упоминает (& sect; 7.21.1/2) в отношении memcpy, что

Если аргумент, объявленный как size_t n, указывает длину массива для  функция n может иметь нулевое значение при вызове этой функции. Если явно не указано в противном случае в описании конкретной функции в этом подпункте аргументы указателя при таком вызове все равно должны иметь действительные значения, как описано в 7.1.4. При таком вызове функция, которая находит символ, не находит никакого вхождения, функция, которая сравнивает две  последовательности символов возвращает ноль, а функция, которая копирует символы, копирует нуль символы.

Указанная здесь ссылка указывает на следующее:

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

Итак, это похоже на C spec, вызывая

memcpy(0, 0, 0)

приводит к поведению undefined, поскольку нулевые указатели считаются "недопустимыми значениями".

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

Ответ 2

Замечания к выпуску для gcc-4.9 указывают на то, что их оптимизатор использует эти правила и, например, может удалить

int copy (int* dest, int* src, size_t nbytes) {
    memmove (dest, src, nbytes);
    if (src != NULL)
        return *src;
    return 0;
}

который затем дает неожиданные результаты при вызове copy(0,0,0) (см. https://gcc.gnu.org/gcc-4.9/porting_to.html).

Я несколько амбивалентен в отношении поведения gcc-4.9; поведение может соответствовать стандартам, но возможность вызова memmove (0,0,0) иногда является полезным расширением этих стандартов.

Ответ 3

Вы также можете рассмотреть использование memmove в Git 2.14.x(Q3 2017)

См. commit 168e635 (16 июля 2017 г.) и commit 1773664, commit f331ab9, commit 5783980 (15 июля 2017 г.) René Scharfe (rscharfe).
(слияние Junio ​​C Hamano - gitster - в commit 32f9025, 11 августа 2017 г.)

Он использует вспомогательный макрос MOVE_ARRAY, который вычисляет размер на основе указанного количества элементов для нас и поддерживает NULL указатели, когда это число равно нулю.
Raw memmove(3) вызовы с помощью NULL can заставит компилятор (более-давно) оптимизировать последующие проверки NULL.

MOVE_ARRAY добавляет безопасный и удобный помощник для перемещения потенциально перекрывающихся диапазонов записей массива.
Он отображает размер элемента, автоматически и безопасно умножает размер байта, выполняет проверку безопасности основного типа, сравнивая размеры элементов и в отличие от memmove(3) он поддерживает указатели NULL, если 0 элементов нужно перемещать.

#define MOVE_ARRAY(dst, src, n) move_array((dst), (src), (n), sizeof(*(dst)) + \
    BUILD_ASSERT_OR_ZERO(sizeof(*(dst)) == sizeof(*(src))))
static inline void move_array(void *dst, const void *src, size_t n, size_t size)
{
    if (n)
        memmove(dst, src, st_mult(size, n));
}

Примеры:

- memmove(dst, src, (n) * sizeof(*dst));
+ MOVE_ARRAY(dst, src, n);

Он использует macro BUILD_ASSERT_OR_ZERO, который утверждает зависимость времени сборки, как выражение (с @cond которое должно быть истинным).
Компиляция завершится неудачно, если условие не является истинным или не может быть оценено компилятором.

#define BUILD_ASSERT_OR_ZERO(cond) \
(sizeof(char [1 - 2*!(cond)]) - 1)

Пример:

#define foo_to_char(foo)                \
     ((char *)(foo)                     \
      + BUILD_ASSERT_OR_ZERO(offsetof(struct foo, string) == 0))

Ответ 4

Нет, memcpy(0,0,0) не является безопасным. Стандартная библиотека, скорее всего, не потерпит неудачу при этом вызове. Однако в тестовой среде в memcpy() может присутствовать дополнительный код для обнаружения переполнения буфера и других проблем. И как эта специальная версия memcpy() реагирует на указатели NULL, ну, в общем, не определено.