Ответ 1
Чтобы иметь возможность указывать указатель на A и переинтерпретировать его как указатель на B, они должны быть взаимно конвертируемыми.
Поинтер-конвертируемый - это объекты, а не типы объектов.
В C++ есть объекты в местах. Если у вас есть Big
в определенном месте с хотя бы одним существующим членом, в этом же месте есть Hdr
из-за взаимной Hdr
указателя.
Однако в этом месте нет объекта Little
. Если там нет Little
объекта, он не может быть конвертируемым указателем с Little
объектом, которого нет.
Они кажутся совместимыми с макетами, предполагая, что они являются плоскими данными (простые старые данные, тривиально копируемые и т.д.).
Это означает, что вы можете скопировать их представление байтов, и оно работает. На самом деле оптимизаторы, похоже, понимают, что memcpy для локального буфера стека, новое место размещения (с тривиальным конструктором), то memcpy back на самом деле является noop.
template<class T>
T* laundry_pod( void* data ) {
static_assert( std::is_pod<Data>{}, "POD only" ); // could be relaxed a bit
char buff[sizeof(T)];
std::memcpy( buff, data, sizeof(T) );
T* r = ::new( data ) T;
std::memcpy( data, buff, sizeof(T) );
return r;
}
вышеприведенная функция является noop во время выполнения (в оптимизированной сборке), но при этом она преобразует данные, совместимые с T-макетами, в data
до фактического T
Итак, если я прав, а Big
and Little
совместимы с макетами, когда Big
является подтипом типов в Little
, вы можете сделать это:
Little* inplace_to_little( Big* big ) {
return laundry_pod<Little>(big);
}
Big* inplace_to_big( Little* big ) {
return laundry_pod<Big>(big);
}
или же
void given_big(Big& big) { // cannot be const
switch(big.h.type) {
case B::type: // fallthrough
case C::type:
auto* little = inplace_to_little(&big); // replace Big object with Little inplace
given_b_or_c(*little);
inplace_to_big(little); // revive Big object. Old references are valid, barring const data or inheritance
break;
// ... other cases here ...
}
}
если Big
имеет не-плоские данные (например, ссылки или const
данные), то вышеизложенное прерывается ужасно.
Обратите внимание, что laundry_pod
не выполняет никакого распределения памяти; он использует новое место размещения, которое создает T
в том месте, где data
указывают байты data
. И хотя похоже, что он делает много вещей (копируя память вокруг), он оптимизируется для noop.
c++ имеет понятие "объект существует". Существование объекта почти не имеет никакого отношения к тому, какие биты или байты написаны на физическом или абстрактном компьютере. В вашем двоичном коде нет инструкции, которая соответствует "теперь объект существует".
Но язык имеет эту концепцию.
Объекты, которые не существуют, не могут взаимодействовать. Если вы это сделаете, стандарт C++ не определяет поведение вашей программы.
Это позволяет оптимизатору делать предположения о том, что делает ваш код и что он не делает, и какие ветки не могут быть достигнуты, и которые могут быть достигнуты. Это позволяет компилятору делать допущения без сглаживания; изменение данных с помощью указателя или ссылки на A не может изменить данные, достигнутые с помощью указателя или ссылки на B, если каким-либо образом оба A и B не находятся в одном и том же месте.
Компилятор может доказать, что объекты Big
и Little
не могут существовать в одном и том же месте. Поэтому никакая модификация любых данных с помощью указателя или ссылки на Little
не могла бы изменить что-либо существующее в переменной типа Big
. И наоборот.
Представьте, если given_b_or_c
изменяет поле. Ну компилятор мог given_big
и given_b_or_c
и use_a_b
, заметить, что ни один экземпляр Big
не был изменен (просто экземпляр Little
), и докажите, что поля данных из Big
он кэшировал до вызова вашего кода, не могут быть изменены.
Это сохраняет инструкцию по загрузке, и оптимизатор вполне доволен. Но теперь у вас есть код, который гласит:
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
if (b.foo!=4) exit(-1);
который оптимизирован для
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
exit(-1);
потому что он может доказать, что b.foo
должен быть 7
он был установлен один раз и никогда не был изменен. Доступ через Little
не мог изменить Big
из-за правил псевдонимов.
Теперь сделайте следующее:
Big b = whatever;
b.foo = 7;
(*laundry_pod<Little>(&b)).foo = 4;
Big& b2 = *laundry_pod<Big>(&b);
if (b2.foo!=4) exit(-1);
и предполагается, что большой там не изменился, потому что есть memcpy и a ::new
которые могли бы законным образом изменить состояние данных. Нет строгого нарушения псевдонимов.
Он все еще может следовать за memcpy
и устранять его.
Живой пример оптимизации laundry_pod
. Обратите внимание, что если он не был оптимизирован, код должен иметь условный и printf. Но поскольку он был, он был оптимизирован в пустую программу.