Это никогда не будет законным, независимо от того, какие уродства вы выполняете со странными приведениями и союзами и еще много чего.
Фундаментальный факт заключается в следующем: два объекта различного типа никогда не могут иметь псевдоним в памяти, за некоторыми исключениями (см. Далее).
Пример
Рассмотрим следующий код:
void sum(double& out, float* in, int count) {
for(int i = 0; i < count; ++i) {
out += *in++;
}
}
Давайте разберем это на переменные локального регистра, чтобы более точно смоделировать фактическое выполнение:
void sum(double& out, float* in, int count) {
for(int i = 0; i < count; ++i) {
register double out_val = out; // (1)
register double in_val = *in; // (2)
register double tmp = out_val + in_val;
out = tmp; // (3)
in++;
}
}
Предположим, что (1), (2) и (3) представляют чтение, чтение и запись в памяти соответственно, что может быть очень дорогостоящими операциями в таком тесном внутреннем цикле. Разумная оптимизация для этого цикла была бы следующей:
void sum(double& out, float* in, int count) {
register double tmp = out; // (1)
for(int i = 0; i < count; ++i) {
register double in_val = *in; // (2)
tmp = tmp + in_val;
in++;
}
out = tmp; // (3)
}
Эта оптимизация уменьшает вдвое количество операций чтения в память и количество операций записи в память до 1. Это может оказать огромное влияние на производительность кода и является очень важной оптимизацией для всех оптимизирующих компиляторов C и C ++.
Теперь предположим, что у нас нет строгого алиасинга. Предположим, что запись в объект любого типа может повлиять на любой другой объект. Предположим, что запись в double может повлиять на значение типа float. Это делает вышеупомянутую оптимизацию подозрительной, поскольку вполне возможно, что программист на самом деле намеревался использовать псевдоним вне и так, чтобы результат функции суммы был более сложным и зависел от процесса. Звучит глупо? Несмотря на это, компилятор не может различить «глупый» и «умный» код. Компилятор может различать только правильно сформированный и плохо сформированный код. Если мы разрешаем свободный псевдоним, то компилятор должен быть консервативным в своих оптимизациях и должен выполнять дополнительное хранилище (3) на каждой итерации цикла.
Надеюсь, теперь вы понимаете, почему такой союз или трюк не может быть законным. Вы не можете обойти такие фундаментальные понятия ловкостью рук.
Исключения из строгого алиасинга
Стандарты C и C ++ предусматривают специальное условие для псевдонимов любого типа с char
и с любым «связанным типом», который среди прочего включает в себя производные и базовые типы, и члены, потому что он может использовать адрес члена класса независимо друг от друга это так важно. Вы можете найти исчерпывающий список этих положений в этом ответе.
Кроме того, GCC предусматривает специальное положение для чтения от члена профсоюза, отличного от того, к которому последний раз писали. Обратите внимание, что этот вид преобразования через объединение фактически не позволяет вам нарушать псевдонимы. Только один член объединения может быть активным в любой момент времени, поэтому, например, даже с GCC следующее поведение будет неопределенным:
union {
double d;
float f[2];
};
f[0] = 3.0f;
f[1] = 5.0f;
sum(d, f, 2); // UB: attempt to treat two members of
// a union as simultaneously active
Обходные
Единственный стандартный способ интерпретировать биты одного объекта как биты объекта другого типа - это использовать эквивалент memcpy
. Это позволяет использовать специальное положение для псевдонимов с char
объектами, что позволяет вам читать и изменять базовое представление объектов на уровне байтов. Например, следующее является законным и не нарушает строгих правил наложения имен:
int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
memcpy(a, &d, sizeof(d));
Это семантически эквивалентно следующему коду:
int a[2];
double d;
static_assert(sizeof(a) == sizeof(d));
for(size_t i = 0; i < sizeof(a); ++i)
((char*)a)[i] = ((char*)&d)[i];
GCC обеспечивает чтение из неактивного члена объединения, неявно делая его активным. Из документации GCC:
Распространена практика чтения от члена профсоюза, отличного от того, к которому недавно был написан (так называемый "тип-наказание"). Даже с параметром -fstrict-aliasing допускается перетаскивание типов при условии, что доступ к памяти осуществляется через тип объединения. Таким образом, приведенный выше код будет работать как положено. См. Перечисления структурных объединений и реализация битовых полей. Тем не менее, этот код может не:
int f() {
union a_union t;
int* ip;
t.d = 3.0;
ip = &t.i;
return *ip;
}
Аналогичным образом, доступ по взятию адреса, приведению результирующего указателя и разыменованию результата имеет неопределенное поведение, даже если приведение использует тип объединения, например ::1010*
int f() {
double d = 3.0;
return ((union a_union *) &d)->i;
}
Размещение новых
(примечание: здесь у меня память, так как сейчас у меня нет доступа к стандарту).
После размещения нового объекта в буфере хранения время жизни базовых объектов хранения неявно заканчивается. Это похоже на то, что происходит, когда вы пишете члену профсоюза:
union {
int i;
float f;
} u;
// No member of u is active. Neither i nor f refer to an lvalue of any type.
u.i = 5;
// The member u.i is now active, and there exists an lvalue (object)
// of type int with the value 5. No float object exists.
u.f = 5.0f;
// The member u.i is no longer active,
// as its lifetime has ended with the assignment.
// The member u.f is now active, and there exists an lvalue (object)
// of type float with the value 5.0f. No int object exists.
Теперь давайте посмотрим на что-то похожее с новым размещением:
#define MAX_(x, y) ((x) > (y) ? (x) : (y))
// new returns suitably aligned memory
char* buffer = new char[MAX_(sizeof(int), sizeof(float))];
// Currently, only char objects exist in the buffer.
new (buffer) int(5);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the underlying storage objects.
new (buffer) float(5.0f);
// An object of type int has been constructed in the memory pointed to by buffer,
// implicitly ending the lifetime of the int object that previously occupied the same memory.
Этот вид неявного окончания срока службы может происходить только для типов с тривиальными конструкторами и деструкторами по очевидным причинам.