Я сталкиваюсь с путаницей в отношении правила строгого псевдонима C ++ и его возможных последствий. Рассмотрим следующий код:
int main() {
int32_t a = 5;
float* f = (float*)(&a);
*f = 1.0f;
int32_t b = a; // Probably not well-defined?
float g = *f; // What about this?
}
Глядя на спецификации C ++, раздел 3.10.10, технически ни один из приведенного кода не нарушает приведенные там «правила алиасинга»:
Если программа пытается получить доступ к сохраненному значению объекта через значение lvalue, отличное от одного из следующих типов, поведение не определено:
… список квалифицированных типов доступа …
*f = 1.0f;
не нарушает правила, потому что нет доступа к сохраненное значение, я просто пишу в память через указатель. Я не читаю по памяти и не пытаюсь интерпретировать значение здесь.int32_t b = a;
не нарушает правила, потому что я получаю доступ через его оригинальный тип.float g = *f;
не нарушает правила по той же причине.В другая нить, член CortAmmon фактически делает ту же точку в ответе и добавляет, что любое возможное неопределенное поведение, возникающее при записи в живые объекты, как в *f = 1.0f;
, будет учтено определением стандарта «время жизни объекта» (которое кажется тривиальным для типов POD).
ОДНАКО: есть много доказательств в Интернете, что приведенный выше код будет производить UB на современных компиляторах. Увидеть Вот а также Вот например.
Аргументация в большинстве случаев заключается в том, что компилятор может &a
а также f
как не совмещать друг друга и, следовательно, свободно перенести инструкции.
Большой вопрос сейчас заключается в том, будет ли такое поведение компилятора «чрезмерной интерпретацией» стандарта.
Единственный раз, когда в стандарте конкретно говорится об «псевдонимах», есть сноска к 3.10.10, где ясно указывается, что именно эти правила должны регулировать псевдонимы.
Как я упоминал ранее, я не вижу ни одного из приведенного выше кода, нарушающего стандарт, однако многие люди (и, возможно, люди, занимающиеся компиляцией) считают его незаконным.
Я был бы очень признателен за разъяснения.
Небольшое обновление:
Как правильно указал член BenVoigt, int32_t
может не совпадать с float
на некоторых платформах, поэтому данный код может нарушать правило «хранения достаточного выравнивания и размера». Я хотел бы заявить, что int32_t
был выбран намеренно, чтобы соответствовать float
на большинстве платформ и что допущение для этого вопроса заключается в том, что типы действительно совпадают.
Небольшое обновление № 2:
Как указали несколько членов, линия int32_t b = a;
вероятно, в нарушение стандарта, хотя и не с абсолютной уверенностью. Я согласен с этой точкой зрения и, не меняя ни одного аспекта вопроса, прошу читателей исключить эту строку из моего утверждения выше о том, что ни один из кодов не является нарушением стандарта.
Вы ошибаетесь в своем третьем пункте (и, возможно, в первом тоже).
Вы заявляете «Линия float g = *f;
не нарушает правила по одной и той же причине. «, где» одна и та же причина «(немного расплывчато), по-видимому, относится к» доступу через оригинальный тип «. Но это не то, что вы делаете. доступ к int32_t
(названный a
) через lvalue типа float
(получено из выражения *f
). Итак, вы нарушаете стандарт.
Я также полагаю (но менее уверен в этом), что сохранение значения — это доступ к (этому) сохраненному значению, так что даже *f = 1.0f;
нарушает правила.
Я думаю, что это утверждение неверно:
Строка int32_t b = a; не нарушает правила, потому что я получаю доступ через его оригинальный тип.
Объект, который хранится в местоположении &a
теперь является float, поэтому вы пытаетесь получить доступ к сохраненному значению float через lvalue неправильного типа.
Есть некоторые существенные неоднозначности в спецификации времени жизни и доступа к объекту, но здесь есть некоторые проблемы с кодом в соответствии с моим прочтением спецификации.
float* f = (float*)(&a);
Это выполняет reinterpret_cast
и до тех пор, пока float
не требует более строгого выравнивания, чем int32_t
Затем вы можете привести полученное значение обратно к int32_t*
и вы получите оригинальный указатель. Использование результата не определено иначе в любом случае.
*f = 1.0f;
Если предположить, *f
псевдонимы с a
(и что хранилище для int32_t
имеет соответствующее выравнивание и размер для float
) то вышеупомянутая строка заканчивает время жизни int32_t
объект и помещает float
объект на своем месте:
Время жизни объекта типа T начинается, когда: получено хранилище с правильным выравниванием и размером для типа T, и если объект имеет нетривиальную инициализацию, его инициализация завершена.
Время жизни объекта типа T заканчивается, когда: […] хранилище, которое занимает объект, используется повторно или освобождается.
—3.8 Время жизни объекта [basic.life] / 1
Мы повторно используем хранилище, но если int32_t
имеет такие же требования к размеру и выравниванию, как кажется float
всегда существовал в одном и том же месте (поскольку хранилище было «получено»). Возможно, мы можем избежать этой двусмысленности, изменив эту строку на new (f) float {1.0f};
так что мы знаем, что float
Объект имеет время жизни, которое началось во время или до завершения инициализации.
Кроме того, «доступ» не обязательно означает «чтение». Это может означать как чтение, так и запись. Таким образом, запись в исполнении *f = 1.0f;
можно рассматривать как «доступ к хранимому значению» путем его записи, и в этом случае это также нарушение псевдонимов.
Итак, теперь предположим, что объект с плавающей точкой существует и int32_t
Время жизни объекта закончилось:
int32_t b = a;
Этот код обращается к сохраненному значению float
объект через glvalue с типом int32_t
и явно является нарушением псевдонимов. Программа имеет неопределенное поведение в соответствии с 3.10 / 10.
float g = *f;
При условии, что int32_t
имеет правильные требования к выравниванию и размеру, а также указатель f
был получен таким образом, чтобы его использование было четко определено, тогда это должно легально float
объект, который был инициализирован с 1.0f
,
Я усвоил трудный путь, что цитирование 6.5.7 из стандарта C99 бесполезно, не смотря также на 6.5.6. Увидеть этот ответ для соответствующих цитат.
6.5.6 проясняет, что тип объекта может, при определенных обстоятельствах, меняться много раз в течение его срока службы. Может принимать тип значения, которое было совсем недавно. написано к этому. Это действительно полезно.
Нам нужно провести различие между «объявленным типом» и «эффективным типом». Локальная переменная или статическая глобальная имеет объявленный тип. Я думаю, что вы застряли с этим типом на всю жизнь этого объекта. Ты можешь читать от объекта с помощью char *
, но «эффективный тип», к сожалению, не меняется.
Но память вернулась malloc
не имеет «объявленного типа». Это останется верным, пока free
д. У него никогда не будет объявленного типа, но его эффективный тип может изменяться в соответствии с 6.5.6, всегда принимая тип самой последней записи.
Итак, это законно:
int main() {
void * vp = malloc(sizeof(int)+sizeof(float)); // it's big enough,
// and malloc will look after alignment for us.
int32_t *ap = vp;
*ap = 5; // make int32_t the 'effective type'
float* f = vp;
*f = 1.0f; // this (legally) changes the effective type.
// int32_t b = *ap; // Not defined, because the
// effective type is wrong
float g = *f; // OK, because the effective type is (currently) correct.
}
Так что, в основном, пишу malloc
-эд пробел является действительным способом изменить его тип. Но я думаю, что это не дает нам возможность взглянуть на уже существующее через «линзу» нового типа, что может быть интересно; это невозможно, если, я думаю, мы не используем различные char*
исключения для просмотра данных «неправильного» типа.