Я хочу понять, является ли следующий код (всегда, иногда или никогда) четко определенным в соответствии с C11:

#include <string.h>
int main() {
  char d[5];
  char s[4] = "abc";
  char *p = s;
  strncpy(d, p, 4);
  p += 4; // one-past end of "abc"
  strncpy(d+4, p, 0); // is this undefined behavior?
  return 0;
}

C11 7.24.2.4.2 говорит:

Функция strncpy копирует не более n символов (символы, следующие за нулевым символом, не копируются) из массива, на который указывает s2, в массив, на который указывает s1.

Обратите внимание, что s2 является массивом , а не строкой (поэтому отсутствие нулевого символа конца при p == s+4 не является проблемой).

Здесь применяется 7.24.1 (соглашения о строковых функциях) (выделено мной):

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

Соответствующая часть вышеупомянутого 7.1.4 (выделено мной):

7.1.4 Использование библиотечных функций

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

У меня проблемы с синтаксическим анализом последней части. «Вычисления всех адресов и доступ к объектам» кажется тривиальным удовлетворением при n == 0, если я могу предположить, что моя реализация не будет вычислять адреса в этом случае.

Другими словами, при строгой интерпретации стандарта всегда следует отказываться от программы? Должен ли я всегда позволять это? Или его правильность зависит от реализации (т.е. если реализация вычисляет адрес первого символа перед проверкой n, то в приведенном выше коде есть UB, в противном случае - нет)?

9
anol 27 Июл 2017 в 15:13
1
IANALL, но я не думаю, что разговор о вычислении адреса перед проверкой n уместен - вы уже вычислили адрес в настройке p. Вопрос в том, (а) действительно ли (это вычисленное) значение p и (б) используется ли оно (когда n равно нулю). Насколько я помню (но я не эксперт), адрес первого элемента за массивом (т.е. s+4) является действительным адресом < i> при условии , что вы не пытаетесь получить доступ к тому, что там есть.
 – 
TripeHound
27 Июл 2017 в 15:23
1
Для справки, подпись функции спецификации: char *strncpy(char * restrict s1, const char * restrict s2, size_t n); Я думаю, это должно быть частью сообщения.
 – 
chux - Reinstate Monica
27 Июл 2017 в 15:57
2
Деталь «Обратите внимание, что s2 - это массив ...» должно быть «Обратите внимание, что s2 указывает на массив». s2 - это указатель, а не массив.
 – 
chux - Reinstate Monica
27 Июл 2017 в 16:12
Выделенный жирным шрифтом текст «Если аргумент функции описан как массив» относится к «Обратите внимание, что s2 указывает на массив».
 – 
M.M
27 Июл 2017 в 16:52
Аналогичным пограничным случаем будет const char foo = "abc"; char *bar = "xyz"; strncpy(fool, bar, 0);. Если счетчик равен 1, это UB для записи в const char *, но с 0 и без записи, это UB? Я ожидаю, что это будет UB, поскольку strncpy(any_const_char_star, blah, blah) определенно UB. Все-таки интересный пост на крайнем случае, который у вас есть.
 – 
chux - Reinstate Monica
27 Июл 2017 в 18:13

4 ответа

char *strncpy(char * restrict s1, const char * restrict s2, size_t n);

Функция strncpy копирует не более n символов (...) из массива, на который указывает s2 "C11 §7.24.4.5 3

Детали strncpy() не полностью отвечают на "strncpy(d, s, 0) с односторонним указателем". Конечно, доступ к *s2 не ожидается, но должен ли доступ к *s2 быть действительным с n==0?

Также не работает 7.24.1 (соглашения о строковых функциях) .

7.1.4 Использование библиотечных функций отвечает, в зависимости от того, применяется ли часть () частично или полностью к предыдущему "то и это"

... Если аргумент функции описывается как массив, указатель, фактически переданный функции, должен иметь такое значение, чтобы все вычисления адресов и доступы к объектам (< strong>, которые были бы действительны, если бы указатель указывал на первый элемент такого массива ) на самом деле действительны ....

  1. Если "(это было бы действительным, если бы указатель указывал на первый элемент такого массива)" применяется только к "доступам к объектам", то strncpy(d, s, 0) в порядке, поскольку значение указателя не должно иметь массив характеристик. Это просто должно быть допустимое вычислимое значение.

  2. Если «(это было бы действительным, если бы указатель действительно указывал на первый элемент такого массива)» применяется также к «вычислениям адреса», то strncpy(d, s, 0) - это UB, поскольку значение указателя должно иметь array < / em> характеристики. который включает в себя вычисление действительного адреса за один проход s. Тем не менее, действительный адрес вычислений с однократной передачей не определен, если само значение s является однопроходным.

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

Второе - возможное чтение, но с натяжкой.

3
Community 20 Июн 2020 в 12:12
" доступ к объектам (который был бы допустим, если бы указатель действительно указывал на первый элемент такого массива) " <- и доступ к первому элементу массива всегда действителен. " значение указателя не обязательно должно иметь характеристики массива " <- почему? Аргумент strncpy(): « описывается как массив » ...
 – 
user2371524
27 Июл 2017 в 16:43

Часть, которую вы выделили:

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

Дает понять, что ваш код действительно недействителен. В части, посвященной нулевому аргументу size_t:

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

Нет гарантии, что функция копирования не попытается получить доступ к чему-либо.

Итак, если посмотреть на это «с другой стороны», следующая реализация strncpy() будет соответствовать:

char *strncpy(char *s1, const char *s2, size_t n)
{
    size_t i = 0;
    char c = *s2;

    while (i < n)
    {
        if (c) c = s2[i];
        s1[i++] = c;
    }
    return s1;
}

Конечно, это глупый код, разумная реализация, например, просто инициализируйте char c = 1, поэтому я буду удивлен, если вы найдете реализацию C в дикой природе, которая будет демонстрировать неожиданное поведение для вашего кода.


Есть еще один аргумент, подтверждающий, что соответствующей реализации разрешен доступ к *s2 в любом случае: массивы нулевого размера не разрешены в C. Поэтому, если s2 должен быть указателем на массив, *s2 должен быть действительным. Это тесно связано с формулировкой цитируемого вами §7.1.4

2
27 Июл 2017 в 16:35
@chux: Эээээ, верно ... позвольте мне попытаться создать действительно соответствующую версию ...
 – 
user2371524
27 Июл 2017 в 16:07
Я не вижу ничего из того, что вы процитировали, что strncpy разрешен доступ к более чем n символам s2. Вы говорите, что ясно, что код недействителен, но мне кажется очевидным, что он действителен.
 – 
interjay
27 Июл 2017 в 16:10
Поскольку s2 должен указывать на массив, а массивы нулевого размера в любом случае не допускаются, он может получить доступ к по крайней мере первому элементу. Цитаты требуют, чтобы доступ был действительным.
 – 
user2371524
27 Июл 2017 в 16:11
И @interjay в стандарте ничего не говорится о том, что для параметра размера 0 доступа не происходит. Он говорит только о том, что скопировано 0 символов, а это другое дело.
 – 
user2371524
27 Июл 2017 в 16:14
[Написано, когда Феликс опубликовал свое] Я считаю, что "раздвоение волос" закончилось тем фактом, что в стандарте сказано, что никакие символы не копируются , если n==0, а не обязательно что никакие символы не читаются ... Как продемонстрировал Феликс, возможно (хотя и глупо) написать функцию, которая считывает байт, а затем решает, копировать ли его . (@Felix: лучшим примером может быть перемещение строки c = ... в цикле на одну строку вниз, чтобы вы по крайней мере использовали значение c, которое вы предварительно загружается вне цикла).
 – 
TripeHound
27 Июл 2017 в 16:15

Адрес, вычисленный p + 4, не является недопустимым значением. Явно разрешено указывать на один конец массива (C11 6.5.6 / 8), и обычное использование - использовать такие указатели в качестве аргументов функции. Итак, код правильный.

Вы заподозрили проблему согласно следующему тексту:

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

Для вызова strncpy с аргументом длины 0 указывается, что никакие символы не копируются, поэтому нет доступа к объектам. Это может включать добавление 0 к указателю, но это четко определено, чтобы добавить 0 к указателю, проходящему мимо конца.

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

2
M.M 27 Июл 2017 в 16:46
Что бы ни возвращал malloc(0), это не указатель на массив. Он может вернуть указатель «, как если бы размер был ненулевым значением, за исключением того, что возвращенный указатель не должен использоваться для доступа к объекту. » (так что не -нулевой размер и доступ ко всему, что он не разрешен)
 – 
user2371524
27 Июл 2017 в 17:36
« Я думаю, что разумно рассматривать цитируемый выше текст как намерение включить указатель« прошедший конец ». » это разумное предположение, что это было предназначено , но дело не в словах ...
 – 
user2371524
27 Июл 2017 в 17:37
Важно намерение, а не точная формулировка. Отчеты о дефектах могут быть отправлены, если формулировка не отражает намерения, это случалось много раз.
 – 
M.M
28 Июл 2017 в 00:47
Что касается вопроса языкового юриста, я не думаю, что предполагаемое намерение должно иметь значение. Конечно, формулировка стандарта должна быть улучшена, если она не отражает намерения, и DR является способом вызвать это. Но с нынешней формулировкой показанный код будет недействительным.
 – 
user2371524
28 Июл 2017 в 09:05

Удивительно, но стандарт никогда не определял, что такое массив. Он определяет, что такое объект массива , но ясно, что определение strncpy не может означать объекты массива. Во-первых, потому что типы неверны (указатель на объект массива не может иметь тип char*). Во-вторых, потому что при такой интерпретации невозможно будет сколько-нибудь полезной манипуляции со строками. Действительно, strncpy (p, s+1, n) станет всегда недействительным , потому что s+1 никогда не указывает на фактический объект массива.

Следовательно, если мы хотим создать реализацию C, которая хотя бы незначительно полезна, мы должны принять другую интерпретацию «массива, на который указывает» (не только в определении strncpy, но и везде в стандарте, где встречается такая фраза). А именно, у этих слов нет выбора, кроме как обозначать часть объекта массива, которая начинается с элемента, на который фактически указывает указатель. Когда указатель указывает за конец массива, рассматриваемая часть имеет нулевой размер.

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

Разрешено принять эту интерпретацию или нет, выходит за рамки этого ответа.

1
n. 'pronouns' m. 25 Авг 2017 в 00:08
1
Re. «Стандарт никогда не определял, что такое массив», Я спросил об этом здесь < / a> но, похоже, никто не знал
 – 
M.M
25 Авг 2017 в 00:25