35 votes

Réinterpréter un syndicat dans un autre syndicat

J'ai un modèle de mise en page de l'union qui a tout un tas de types à:

union Big {
    Hdr h;

    A a;
    B b;
    C c;
    D d;
    E e;
    F f;
};

Chacun des types d' A thru F est la norme-mise en page et a son premier membre d'un objet de type Hdr. L' Hdr identifie ce que le membre actif de l'union, c'est donc la variante de-comme. Maintenant, je suis dans une situation où je sais que pour certains (parce que j'ai vérifié) que le membre actif est soit une B ou C. Effectivement, j'ai réduit l'espace pour:

union Little {
    Hdr h;

    B b;
    C c;
};

Maintenant, est le suivant bien défini ou indéfini comportement?

void given_big(Big const& big) {
    switch(big.h.type) {
    case B::type: // fallthrough
    case C::type:
        given_b_or_c(reinterpret_cast<Little const&>(big));
        break;
    // ... other cases here ...
    }
}

void given_b_or_c(Little const& little) {
    if (little.h.type == B::type) {
        use_a_b(little.b);
    } else {
        use_a_c(little.c);
    }
}

L'objectif de l' Little est de bien servir que de la documentation, que j'ai déjà vérifié que c'est un B ou C dans l'avenir personne ne ajoute le code pour vérifier que c'est un A ou quelque chose.

C'est le fait que je suis la lecture de l' B sous-objet en tant que B assez pour faire ce bien formé? Peut la commune de la séquence initiale de la règle de façon significative à utiliser ici?

18voto

Yakk Points 31636

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.


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.

4voto

Martin Bonner Points 91

Je ne trouve pas de formulation dans n4296 (projet de C++14 standard) qui pourrait la rendre légale. Ce qui est plus, je ne peux même trouver un libellé qui donné:

union Big2 {
    Hdr h;

    A a;
    B b;
    C c;
    D d;
    E e;
    F f;
};

nous avons peut - reinterpret_cast une référence à l' Big dans une référence à l' Big2 , puis utiliser la référence. (À noter qu' Big et Big2 sont de mise en page compatible.)

2voto

T.C. Points 22510

C'est UB par omission. [expr.ref]/4.2:

Si E2 est un non-membre de données statiques et le type de E1 est "cq1 vq1 X", et le type de E2 est "cq2 vq2 T", l'expression [E1.E2] désigne le nommé membre de l'objet désigné par la première expression.

Lors de l'évaluation de l' given_b_or_c appel en given_big, l'objet de l'expression en little.h n'a pas fait de désigner un Little objet, et ergo il n'y a pas de membre. Parce que la norme "omet aucune définition explicite de comportement" pour ce cas, le comportement est indéfini.

-1voto

Olaf Dietsche Points 35264

Je ne suis pas sûr, si cela s'applique vraiment ici. Dans l' reinterpret_cast - Notes de l'article, ils parlent de pointeur-interconvertibles objets.

Et à partir [de base.composé]/4:

Deux objets a et b sont de pointeur interconvertibles si:

  • ils sont le même objet, ou
  • l'un est une union de l'objet et l'autre est un non-membre de données statiques de l'objet, ou
  • l'un est un modèle de mise en page de la classe de l'objet, et l'autre est le premier non-donnée membre statique de l'objet, ou, si l'objet n'a pas non statique de données de membres, la première classe de base sous-objet d'un objet, ou
  • il existe un objet de c telles que a et c sont de pointeur interconvertibles, et c et b sont de pointeur interconvertibles.

Si les deux objets sont de pointeur interconvertibles, alors ils ont la même adresse, et il est possible d'obtenir un pointeur sur l'un à partir d'un pointeur à l'autre par l'intermédiaire d'un reinterpret_­cast.

Dans ce cas, nous avons Hdr h; (c) en tant que non-membre de données statiques dans les deux syndicats, qui devrait permettre à (à cause de la deuxième et dernier point)

Big* (a) -> Hdr* (c) -> Little* (b)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X