Pour être en mesure de prendre un pointeur vers Un, et de les réinterpréter comme un pointeur vers B, ils doivent être pointeur-interconvertibles.
Pointeur-interconvertibles est à propos des objets, pas des types d'objets.
En C++, il y a des objets à des endroits. Si vous avez un Big
à un endroit particulier avec au moins un membre existant, il y a aussi un Hdr
à la même place en raison de pointeur interconvertability.
Cependant il n'y a pas d' Little
objet à cet endroit. Si il n'y a pas d' Little
objet, il ne peut pas être un pointeur-interconvertibles avec un Little
objet qui n'est pas là.
Ils semblent être la mise en page-compatible, en supposant qu'elles sont données à plat (plain old données, trivialement copiable, etc).
Cela signifie que vous pouvez copier leurs octets de la représentation , et cela fonctionne. En fait, les optimiseurs de l'air de comprendre qu'un memcpy à une pile tampon local, un placement de nouvelles (avec trivial constructeur), puis un memcpy dos est en fait un 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;
}
la fonction ci-dessus est un noop au moment de l'exécution (l'optimisation de construire), mais il convertit T-présentation des données compatibles à l' data
réel T
.
Donc, si je suis droite et Big
et Little
mise en page sont compatibles lorsqu' Big
est un sous-type de types en Little
, vous pouvez faire ceci:
Little* inplace_to_little( Big* big ) {
return laundry_pod<Little>(big);
}
Big* inplace_to_big( Little* big ) {
return laundry_pod<Big>(big);
}
ou
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 ...
}
}
si Big
a non plane de données (comme des références ou const
des données), les pauses horriblement.
Notez que laundry_pod
ne fait pas d'allocation de mémoire; il utilise le placement de nouvelles qui construit un T
dans l'endroit où l' data
de points à l'aide de la octets en data
. Et bien qu'il semble comme il est de faire beaucoup de choses (la copie de la mémoire autour), il optimise pour un noop.
c++ a une notion de "un objet existe". L'existence d'un objet n'a presque rien à voir avec ce que l'bits ou d'octets sont écrits dans la physique ou de la machine abstraite. Il n'y a pas d'instruction sur votre binaire qui correspond à "maintenant un objet existe".
Mais la langue a ce concept.
Des objets qui n'existent pas ne peut pas être en interaction avec. Si vous le faites, la norme C++ ne permet pas de définir le comportement de votre programme.
Cela permet à l'optimiseur de faire des hypothèses sur ce que votre code et de ne pas faire et les branches qui ne peut pas être atteint et qui peut être atteint. Il permet au compilateur de faire des pas de lissage des hypothèses; la modification de données par l'intermédiaire d'un pointeur ou une référence à Un ne peut pas modifier les données atteint par le biais d'un pointeur ou une référence à B, à moins d'une certaine manière à la fois A et B existent dans le même endroit.
Le compilateur peut prouver qu' Big
et Little
objets ne peuvent à la fois exister au même endroit. Donc pas de modification de toutes les données par le biais d'un pointeur ou d'une référence à l' Little
pourrait modifier quoi que ce soit, existant dans une variable de type Big
. Et vice-versa.
Imaginez si given_b_or_c
modifie un champ. Bien que le compilateur pourrait inline given_big
et given_b_or_c
et use_a_b
, notez qu'aucune instance de Big
est modifié (juste un exemple de Little
), et de prouver que les champs de données à partir d' Big
en cache avant de faire appel à votre code ne peut pas être modifiée.
Cela permet d'économiser une charge de l'instruction et de l'optimiseur est tout à fait heureux. Mais maintenant, vous avez le code qui se lit comme suit:
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
if (b.foo!=4) exit(-1);
c'est optimzied à
Big b = whatever;
b.foo = 7;
((Little&)b).foo = 4;
exit(-1);
parce qu'il peut prouver qu' b.foo
doit 7
il a été mis une fois et jamais modifié. L'accès par le biais Little
ne peut pas modifier l' Big
en raison de l'aliasing règles.
Maintenant ce faire:
Big b = whatever;
b.foo = 7;
(*laundry_pod<Little>(&b)).foo = 4;
Big& b2 = *laundry_pod<Big>(&b);
if (b2.foo!=4) exit(-1);
et il l'assume que le big n'a pas changé, car il y a un memcpy et un ::new
qui pourrait changer légalement de l'état des données. Pas de stricte aliasing violation.
Il peut encore suivre l' memcpy
et de l'éliminer.
Exemple vivant de l' laundry_pod
optimisé loin. Notez que si ce n'était pas du tout optimisé loin, le code devrait avoir une condition, et un printf. Mais parce que c'était, elle a été optimisé dans le programme vide.