Это никогда не будет законным, независимо от того, какие искажения вы совершаете со странными слепками, союзами и прочим.
Фундаментальный факт заключается в следующем: два объекта разных типов могут никогда не иметь псевдонимов в памяти, за некоторыми исключениями (см. Ниже).
Пример
Рассмотрим следующий код:
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 может где-то повлиять на значение числа с плавающей запятой. Это делает вышеупомянутую оптимизацию подозрительной, потому что возможно, что программист на самом деле предназначил для out и in to псевдоним, чтобы результат функции суммы был более сложным и зависел от процесса. Звучит глупо? Даже в этом случае компилятор не может различить «глупый» и «умный» код. Компилятор может различать только правильно сформированный и плохо сформированный код. Если мы разрешаем свободное алиасинг, то компилятор должен быть консервативным в своих оптимизациях и должен выполнять дополнительное хранилище (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;
}
Точно так же доступ путем взятия адреса, приведения результирующего указателя и разыменования результата имеет неопределенное поведение, даже если при приведении используется тип объединения, например:
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.
Теперь давайте посмотрим на что-то похожее с place-new:
#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.
Этот вид неявного завершения срока службы может происходить только для типов с тривиальными конструкторами и деструкторами по очевидным причинам.
person
anttirt
schedule
02.04.2012
dummy_union
, и не должно возникнуть проблем при условии, что выравнивание типов совместимо. - person Johannes Schaub - litb   schedule 01.04.2012u->destination
активным участником? - person Kerrek SB   schedule 01.04.2012double
через lvalue типаdouble
нормально. - person Johannes Schaub - litb   schedule 01.04.2012main
только пишет. Но это не что иное, как сказать*reinterpret_cast<double*>(buf) = 43.1337;
, не так ли? Это то же нарушение псевдонима. Дело в том, чтоbuf
не указатель на подходящий объект объединения. - person Kerrek SB   schedule 01.04.2012int a; float *b = (float*)&a; *b = 1.0f; std::cout << *b;
. Вы читаете объектfloat
по lvalue типаfloat
. - person Johannes Schaub - litb   schedule 01.04.2012*(double*)malloc(sizeof(double)) = 43.1337;
, просто в этом случае у нас есть гарантированные условия выравнивания. В этом случае значение l до записи также не относилось к объектуdouble
. - person Johannes Schaub - litb   schedule 01.04.2012malloc
гарантированно вернет указатель, подходящий для любого типа. Напротив, OP используетint*
, полученный как адрес существующего объектаint
. - person Kerrek SB   schedule 01.04.2012&x
для некоторых случайныхx
. - person Kerrek SB   schedule 01.04.2012int
не имеет деструктора (с эффектами), поэтому вы можете использовать эту память, но вы можете в первую очередь использовать ее только для хранения в ней других int. Все остальное требует дополнительных гарантий. - person Kerrek SB   schedule 01.04.2012