Нарушает ли этот код строгое правило псевдонимов?

Вопросы:

  1. Этот код ниже нарушает строгие правила псевдонимов? То есть будет ли разрешено умному компилятору печатать 00000 (или какой-то другой неприятный эффект), потому что к буферу, сначала доступному как к другому типу, затем обращаются через int*?

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

  3. Если нет, сломает ли это удаление фигурных скобок (таким образом, ptr1 и ptr2 находятся в одной области видимости)?

  4. Если да, то как можно исправить код?

Дополнительный вопрос: если код в порядке, а 2. или 3. тоже не нарушают его, как изменить его, чтобы он нарушал строгие правила псевдонимов (например, преобразовать цикл с фигурными скобками для использования int16_t)?


int i;
void *buf = calloc(5, sizeof(int)); // buf initialized to 0

{
    char *ptr1 = buf;    
    for(i = 0; i < 5*sizeof(int); ++i)
        ptr1[i] = i;
}

int *ptr2 = buf;
for(i = 0; i < 5; ++i)
    printf("%d", ptr2[i]);

Ищу подтверждение, поэтому краткий (выше), экспертный ответ об этом конкретном коде, в идеале с минимальными стандартными кавычками, - это то, что мне нужно. Мне не нужны длинные объяснения строгих правил алиасинга, только те части, которые относятся к этому коду. И было бы здорово, если бы ответ явно перечислял пронумерованные выше вопросы.

Также предположим, что ЦП общего назначения не имеет целочисленных значений ловушки, и допустим также, что int - это 32 бита и дополнение до двух.


person hyde    schedule 13.07.2016    source источник


Ответы (2)


Нет, это не так, но это только потому, что память была выделена и записана с использованием символьного типа.

Память выделяется с помощью malloc. Этот объект не имеет объявленного типа1, потому что он был выделен с помощью malloc. Таким образом, объект не имеет эффективного типа.

Затем код обращается к объекту и изменяет его, используя тип char. Поскольку тип 2 char и ни один объект, имеющий эффективный тип, не копируется5, при копировании эффективный тип не устанавливается равным char для этого и последующих обращений, а задается эффективный тип на char, только на время доступа3. После доступа объект больше не имеет эффективного типа.

Затем тип int используется для доступа и только для чтения этого объекта. Поскольку у объекта нет эффективного типа, он становится 3 int на время чтения. После доступа объект больше не имеет эффективного типа. Поскольку int был явно совместим с эффективным типом int, поведение определено.

(Предполагая, что считанные значения не являются представлением ловушки для int.)


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

Допустим, ваш пример был (при условии sizeof(float)==sizeof(int)):

int i;
void *buf = calloc(5, sizeof(float)); // buf initialized to 0

{
    float *ptr1 = buf;    
    for(i = 0; i < 5*sizeof(float); ++i)
        ptr1[i] = (float)i;
}

int *ptr2 = buf;
for(i = 0; i < 5; ++i)
    printf("%d", ptr2[i]);

Эффективный тип объекта, когда записываются float, становится типом float на время записи и всех последующих обращений к объекту, которые не изменяют его2. Когда к этим объектам затем обращается int, эффективный тип остается float, так как значения только считываются, а не изменяются. Предыдущая запись с использованием float навсегда установила эффективный тип float до следующей записи в этот объект (чего в данном случае не произошло). Типы int и float несовместимы4, поэтому поведение не определено.


(Весь приведенный ниже текст взят из: ISO:IEC 9899:201x)

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

2 (6.5 Выражения 6)
Если значение хранится в объекте, не имеющем объявленного типа, через lvalue, имеющее тип, не являющийся символьным типом, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменяют сохраненное значение.

3 (6.5 Выражения 6)
Для всех других обращений к объекту, не имеющему объявленного типа, эффективным типом объекта является просто тип lvalue, используемый для доступа.

4 (6.5 Выражения 8)
Доступ к хранимому значению объекта должен осуществляться только выражением lvalue, имеющим один из следующих типов: 88) — тип, совместимый с действующим типом объекта , — уточненная версия типа, совместимая с действующим типом объекта, — тип, который является подписанным или беззнаковым типом, соответствующим эффективному типу объекта, — тип, который является подписанным или беззнаковым типом, соответствующим уточненному типу версия эффективного типа объекта, - агрегатный тип или тип объединения, который включает один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член подагрегата или содержащегося объединения), или - символьный тип.

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

person 2501    schedule 13.07.2016
comment
Вы неявно поднимаете интересный момент, о котором я не подумал: поскольку выделенная память не назначается через int lvalue, не становится ли она еще int согласно абзацу 6 6.5, когда она разыменовывается через int *, тем самым нарушая строгое сглаживание после всего? Разбирать этот абзац больно. - person Andrew Henle; 13.07.2016
comment
В этот момент в объект был записан только символ, и это не устанавливало эффективный тип объекта. При чтении он становится int, потому что Для всех других обращений к объекту, не имеющему объявленного типа, эффективным типом объекта является просто тип lvalue, используемый для доступа. - person 2501; 13.07.2016
comment
Если бы в тот же момент в объект записывалось int, а не читалось, тип также стал бы int, потому что: Если значение сохраняется в объекте, не имеющем объявленного типа, через lvalue, имеющее тип, который не является символьный тип, то тип lvalue становится эффективным типом объекта для этого доступа и для последующих доступов, которые не изменяют сохраненное значение. - person 2501; 13.07.2016
comment
Итак, если код изменить на char charbuf[5*sizeof(int)] ] = ""; void *buf = charbuf; и удалить calloc, это также сломает его? - person hyde; 13.07.2016
comment
@ 2501 Я думаю, что часть параграфа 6, скопированная как массив символов, применима из-за цикла for в вопросе. Предложение о типе символа в параграфе 7 необходимо, чтобы избежать нарушения строгого совмещения имен. - person Andrew Henle; 13.07.2016
comment
@hyde Да, определенно, автоматические объекты имеют объявленный тип, и эффективный тип не может быть изменен. Из-за цитаты 4. int несовместим с char. - person 2501; 13.07.2016
comment
@AndrewHenle Но мы не копировали из какого-либо объекта: тогда эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют значение, является эффективным типом объекта, из которого копируется значение, если он есть. - person 2501; 13.07.2016
comment
@AndrewHenle Я думаю, что копирование в виде массива символьного типа означает, что значение копируется полностью из другого объекта, что здесь не так (значение int сохраняется через символ указатель, но представление не копируется). К сожалению, это не определено четко в стандарте (один из многих недостатков). - person davmac; 13.07.2016
comment
@2501 Но будет ли цикл for копией серии отдельных объектов char? Таким образом, приводя к параграфу 7, это тип персонажа? - person Andrew Henle; 13.07.2016
comment
Запись символов устанавливает для объекта эффективный тип char только на время записи, а не навсегда, в отличие от примера с плавающей запятой. - person 2501; 13.07.2016

Нет. Это не нарушает строгое использование псевдонимов.

Из Стандарта C, 6.2 .5 Типы, параграф 28:

Указатель на void должен иметь те же требования к представлению и выравниванию, что и указатель на символьный тип. 48

Обратите внимание на 48. Это относится к сноске 48:

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

Таким образом, вы можете без проблем получить доступ к памяти calloc() через указатель char * (при условии, что ваш ptr должен быть ptr1).

Хотя это действительно лишнее, так как 7.22.3 Функции управления памятью, параграф 1 гласит:

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

Таким образом, вы можете безопасно получить доступ к памяти calloc() через указатель int, а также через указатель char. И указатель double для загрузки (при условии, что вы остаетесь в пределах выделенной памяти).

person Andrew Henle    schedule 13.07.2016
comment
Строгий псевдоним — это нечто иное, чем требования к выравниванию и представлению. - person Johannes Schaub - litb; 13.07.2016
comment
@JohannesSchaub-litb Вопрос относится к тому же самому, что делает memset() (Функция memset копирует значение c (преобразованное в unsigned char ) в каждый из первых n символов объекта, на который указывает s .< /i>) Если вы считаете, что это неправильно, опубликуйте свой ответ, в котором также объясняется, как memset() может установить для любого типа памяти повторяющееся значение char. Вы говорите, что memset() нарушает строгое сглаживание? - person Andrew Henle; 13.07.2016
comment
@AndrewHenle никто не говорит, что код в вопросе нарушает строгие правила псевдонимов. Однако обоснование того, почему это так, приведенное в вашем ответе, неверно. Наличие одинакового выравнивания и представления не означает, что два типа могут быть псевдонимами. - person davmac; 13.07.2016
comment
@davmac Но фундаментальная причина строгого псевдонима - выполнение требования о типе, совместимом с эффективным типом объекта, - из-за такого выравнивания и представления. Как совместимы объекты? - person Andrew Henle; 13.07.2016
comment
@AndrewHenle Основная причина строгого псевдонима заключается в том, чтобы ограничить типы lvalue, которым разрешен доступ к объекту (в основном, чтобы разрешить определенные оптимизации компилятора, хотя в стандарте это, конечно, не указано). совместимые типы определены в 6.2.7, где не утверждается, что любые два типа с одинаковым выравниванием и представлением совместимы. Дело не в том, может ли указатель указывать на объект или нет; strict aliasing касается того, разрешено ли разыменовывать такой указатель, при условии, что он может быть создан. - person davmac; 13.07.2016
comment
@AndrewHenle Если это не фундаментально, то, по крайней мере, важная причина для строгого правила псевдонима - разрешить оптимизацию. Без строгого правила алиасинга компилятор должен был бы предположить, что почти любое присвоение любой переменной может изменить даже энергонезависимое значение, на которое указывает указатель (и пришлось бы повторно извлекать его из памяти), и наоборот, почти любое присвоение через указатель может изменить значение любой переменной. Строгие правила псевдонимов позволяют компилятору предполагать, что значения не изменяются таким образом, в тех случаях, когда это применимо. - person hyde; 13.07.2016
comment
(т. е. строгие правила алиасинга в основном воплощены в 6.5, параграф 7). - person davmac; 13.07.2016
comment
... когда-либо намеревался использовать Стандарт в качестве предлога для авторов компиляторов, чтобы изо всех сил не поддерживать полезные функции и гарантии, которые исторически предоставлялись на платформах, подобных цели, но авторы gcc, похоже, считают, что любой код, использующий поведение, не предусмотренное Стандартом, не работает, даже если рассматриваемое поведение последовательно обрабатывалось в течение десятилетий на всех платформах, даже отдаленно похожих на цель. - person supercat; 14.07.2016
comment
@hyde: Это действительно цель. хотя, если только авторы стандарта не лукавили с примером, приведенным в обосновании, я не вижу причин полагать, что они хотели, чтобы программисты прыгали через обручи, чтобы использовать каламбур типов там, где это было полезно и нетупой компилятор признать, что это было вероятно. Учитывая foo(int *p, int *r, int *s) { float *fp; *r=1; fp = (float*)p; *fp= 0.0f; *s=2; *fp=0.0f; return *r+*s; }, я думаю, они ожидали, что компилятор может пропустить то, что вторая запись в *fp может привести к int, но обратите внимание на приведение int* к float*... - person supercat; 14.07.2016
comment
... как указание на то, что любые значения int, адрес которых может быть раскрыт, должны быть удалены из регистров, когда происходит приведение типов. Компилятор может быть недостаточно изощренным, чтобы распознать тот факт, что указатель приведения может продолжать существовать после последующего использования значений int, но, поскольку основное использование таких приведений типов будет заключаться в выполнении немедленного каламбура типов, предполагается, что приведение указывает на немедленное использование значений int. алиасинг, вероятно, не следует считать чрезмерно пессимистичным. К сожалению, Стандарт предполагает, что у авторов компиляторов достаточно здравого смысла, чтобы... - person supercat; 14.07.2016
comment
... им не понадобятся авторы Стандарта, чтобы констатировать очевидное. - person supercat; 14.07.2016