web-dev-qa-db-ja.com

ユニオンと型パニング

しばらく探していましたが、明確な答えが見つかりません。

多くの人々は、共用体を使用して型を打つことは未定義であり、悪い習慣であると言います。どうしてこれなの?元の情報を書き込むメモリがそれ自体の一致を変更するわけではないことを考慮して、未定義の処理を行う理由はわかりません(スタックの範囲外にならない限り、それはユニオンの問題ではありません、それは悪いデザインでしょう)。

人々は厳密なエイリアシング規則を引用していますが、それはあなたにはできないのであなたにはできないと言っているように思えます。

また、しゃれを入力しない場合の結合のポイントは何ですか?異なる時間に異なる情報に同じメモリ位置を使用するために使用されることになっている場所を見ましたが、それを再度使用する前に情報を削除しないのはなぜですか?

要約すると:

  1. 型のパニングに共用体を使用するのはなぜ悪いのですか?
  2. そうでない場合、彼らのポイントは何ですか?

追加情報:主にC++を使用していますが、それとCについて知りたいです。具体的には、ユニオンを使用して浮動小数点数と生の16進数を変換してCANバス経由で送信します。

59
Matthew Wilkins

繰り返しますが、共用体を介した型のパンニングは、Cでは完全に問題ありません(C++ではそうではありません)。対照的に、ポインターキャストを使用すると、C99の厳密なエイリアシングに違反し、タイプごとに異なるアライメント要件があり、間違った場合にSIGBUSを発生させる可能性があるため、問題があります。組合では、これは決して問題ではありません。

C標準からの関連する引用は次のとおりです。

C89セクション3.3.2.3§5:

値がオブジェクトの別のメンバーに格納された後にユニオンオブジェクトのメンバーがアクセスされる場合、動作は実装定義です

C11セクション6.5.2.3§3:

後置式の後に。演算子と識別子は、構造体または共用体オブジェクトのメンバーを指定します。値は、指定されたメンバーの値です

次の脚注95を使用します。

ユニオンオブジェクトの内容を読み取るために使用されるメンバーが、オブジェクトに値を格納するために最後に使用されたメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しい型のオブジェクト表現として再解釈されます6.2.6で説明されています(「タイプパンニング」と呼ばれることもあるプロセス)。これはトラップ表現である可能性があります。

これは完全に明確でなければなりません。


Jamesは、C11セクション6.7.2.1§16の読み取りにより混乱しています。

最大で1つのメンバーの値は、いつでもユニオンオブジェクトに格納できます。

これは矛盾しているように見えますが、そうではありません。C++とは対照的に、Cにはアクティブメンバの概念がなく、互換性のない型の式を介して単一の格納値にアクセスできます。

C11 annex J.1§1も参照してください。

[未指定]に最後に保存されたもの以外のユニオンメンバーに対応するバイトの値。

C99では、これは読み取りに使用されていました

[未指定]に保存されている最後のメンバー以外のユニオンメンバーの値

これは間違っていました。附属書は規範的ではないため、独自のTCを評価せず、次の標準リビジョンが修正されるまで待つ必要がありました。


標準C++(およびC90)へのGNU拡張機能 共用体による型のパニングを明示的に許可 。 GNU拡張機能をサポートしていない他のコンパイラも、ユニオン型のパニングをサポートしている可能性がありますが、基本言語標準の一部ではありません。

38
Christoph

ユニオンの本来の目的は、さまざまなタイプを表すことができるようにしたい場合にスペースを節約することでした。これを variant type の良い例として Boost.Variant を参照この。

他の一般的な使用法は type punning です。これの有効性は議論されていますが、実際にはほとんどのコンパイラがサポートしています。 gccがそのサポートを文書化しています

直近に書かれたものとは別の組合員から読む(「タイプ・パニング」と呼ばれる)慣行は一般的です。 -fstrict-aliasingを使用しても、union型を介してメモリにアクセスする場合、型のパンニングが許可されます。したがって、上記のコードは期待どおりに機能します。

-fstrict-aliasingを使用した場合でもと表示されることに注意してください。タイプパンニングは許可されています。これは、エイリアスの問題があることを示しています。

Pascal Cuoqは、 欠陥レポート28 これがCで許可されていることを明確にしたと主張しました。 欠陥レポート28 は、明確化として次の脚注を追加しました。

ユニオンオブジェクトのコンテンツにアクセスするために使用されるメンバーが、オブジェクトに値を格納するために最後に使用されるメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しいタイプのオブジェクト表現として再解釈されます6.2.6で説明されています(「タイプパンニング」と呼ばれることもあるプロセス)。これはトラップ表現である可能性があります。

c11では脚注95

std-discussionメールグループトピック nionを介したPunningのタイプ 引数が作成されますが、これは指定不足ですが、DR 283は脚注だけで、新しい規範的な表現を追加しませんでした。

これは、私の意見では、Cの不十分なセマンティック泥沼です。実装者とC委員会の間で、どのケースが動作を定義し、どのケースが定義していないかについてコンセンサスに達していない[...]

C++では 動作が定義されているかどうかは不明

この説明では、ユニオンを介した型のパニングを許可することが望ましくない理由の少なくとも1つについても説明します。

[...] C標準の規則は、現在の実装が実行する型ベースのエイリアス分析の最適化を破ります。

それはいくつかの最適化を壊します。これに対する2番目の引数は、memcpyを使用すると同一のコードが生成されるはずであり、最適化と明確に定義された動作を壊さないことです。たとえば次のとおりです。

std::int64_t n;
std::memcpy(&n, &d, sizeof d);

これの代わりに:

union u1
{
  std::int64_t n;
  double d ;
} ;

u1 u ;
u.d = d ;

godboltを使用するとこれは同一のコードを生成します を見ることができ、コンパイラーが同一のコードを生成しない場合はバグと見なされるべきです:

これが実装に当てはまる場合は、バグを報告することをお勧めします。特定のコンパイラでパフォーマンスの問題を回避するために実際の最適化(型ベースのエイリアス分析に基づいたもの)を壊すことは、私にとって悪い考えのようです。

ブログの投稿 Type Punning、Strict Aliasing、およびOptimization も同様の結論に達します。

未定義の振る舞いメーリングリストの議論: コピーを避けるためにpunningと入力 は多くの同じ根拠をカバーしており、領土がどれだけ灰色になるかを見ることができます。

11
Shafik Yaghmour

C99では合法です:

標準から:6.5.2.3構造体およびユニオンメンバー

ユニオンオブジェクトのコンテンツにアクセスするために使用されるメンバーが、オブジェクトに値を格納するために最後に使用されるメンバーと同じでない場合、値のオブジェクト表現の適切な部分は、新しいタイプのオブジェクト表現として再解釈されます6.2.6で説明されています(「タイプパンニング」と呼ばれることもあるプロセス)。これはトラップ表現である可能性があります。

6
David Ranieri

この未定義の動作を行うための2つの変更があります(少なくともC90に戻っていました)。 1つ目は、コンパイラは、ユニオンにあるものを追跡し、間違ったメンバーにアクセスしたときにシグナルを生成する追加のコードを生成できることです。実際には、誰もやったことはないと思います(おそらくCenterLine?)。もう1つは、これによって開かれた最適化の可能性であり、これらが使用されます。私はコンパイラーを使用しました。これは、変数が範囲外になるか、別の値の後続の書き込みがあるため、必要でない可能性があるという理由で、可能な限り最後まで書き込みを延期します。論理的には、ユニオンが表示されたときにこの最適化がオフになると予想されますが、Microsoft Cの初期バージョンではそうではありませんでした。

型の整理の問題は複雑です。 C委員会(1980年代後半に遡る)は多かれ少なかれ、このためにキャストではなくキャスト(C++ではreinterpret_cast)を使用すべきだという立場を取りました。それ以来、一部のコンパイラ(g ++など)は反対の視点を取り、ユニオンの使用をサポートしていますが、キャストの使用はサポートしていません。また、実際には、型のパニングがあることがすぐに明らかでない場合は、どちらも機能しません。これが、g ++の視点の背後にある動機かもしれません。ユニオンメンバにアクセスすると、タイプパンニングが発生する可能性があることがすぐにわかります。しかし、もちろん、次のようなものが与えられます:

int f(const int* pi, double* pd)
{
    int results = *pi;
    *pd = 3.14159;
    return results;
}

と呼ばれる:

union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );

標準の厳密な規則に従って完全に合法ですが、g ++(およびおそらく他の多くのコンパイラ)では失敗します。 fをコンパイルするとき、コンパイラはpipdがエイリアスできないと想定し、*pdへの書き込みと*piからの読み取りを並べ替えます。 (これが保証されるという意図は決してなかったと思います。しかし、標準の現在の文言はそれを保証します。)

編集:

他の答えは、振る舞いが実際に定義されていると主張しているので(大部分は文脈から外れた非規範的ノートの引用に基づいています):

ここでの正しい答えはpablo1977の答えです:型は、punningが関係する場合の動作を定義しようとしません。これの考えられる理由は、定義できる移植可能な動作がないことです。これは、特定の実装がそれを定義することを妨げません。私はこの問題に関する特定の議論を覚えていませんが、意図は実装が何かを定義することであると確信しています(すべてではありませんが、ほとんどがそうします)。

型のパンニングにユニオンを使用することに関して:C委員会がC90を開発していたとき(1980年代後半)、追加のチェック(境界チェックにファットポインターを使用するなど)を実装するデバッグを許可する明確な意図がありました。当時の議論から、意図はデバッグの実装が共用体で初期化された最後の値に関する情報をキャッシュし、他の何かにアクセスしようとするとトラップされる可能性があることは明らかでした。これは、§6.7.2.1/ 16で明確に述べられています。「メンバーの最大1つの値は、いつでもユニオンオブジェクトに格納できます。」存在しない値にアクセスすると、未定義の動作が発生します。初期化されていない変数へのアクセスと同化できます。 (同じタイプの別のメンバーにアクセスすることが合法かどうかについては、いくつかの議論がありました。しかし、最終的な解決策が何であったかはわかりません。1990年頃からC++に移行しました。)

C89からの引用に関して、動作は実装定義であると言っています。セクション3(用語、定義、および記号)でそれを見つけることは非常に奇妙に思えます。自宅で私のC90のコピーで調べる必要があります。標準の以降のバージョンで削除されたという事実は、その存在が委員会によってエラーと見なされたことを示唆しています。

標準がサポートする共用体の使用は、派生をシミュレートする手段としてです。以下を定義できます。

struct NodeBase
{
    enum NodeType type;
};

struct InnerNode
{
    enum NodeType type;
    NodeBase* left;
    NodeBase* right;
};

struct ConstantNode
{
    enum NodeType type;
    double value;
};
//  ...

union Node
{
    struct NodeBase base;
    struct InnerNode inner;
    struct ConstantNode constant;
    //  ...
};

そして、Nodeがinnerで初期化された場合でも、base.typeに合法的にアクセスします。(6.5.2.3/6が「1つの特別な保証が行われるという事実。これを明示的に許可することは、他のすべてのケースが未定義の動作であることを意味する非常に強力な兆候です。そしてもちろん、「未定義の動作は、この国際標準では '' undefined振る舞い」または§4/ 2の振る舞いの明示的な定義の省略による;振る舞いが未定義ではないと主張するためには、標準のどこで定義されているかを示す必要があります。 )

最後に、型のパニングに関しては、すべて(または少なくとも私が使用したすべての)実装が何らかの方法でサポートしています。当時の私の印象は、ポインタのキャストが実装がそれをサポートする方法であるという意図だったということです。 C++標準では、reinterpret_castの結果が基礎となるアーキテクチャに精通している人に「驚くべき」ことを示唆する(非規範的な)テキストさえあります。ただし、実際には、アクセスがユニオンメンバを介している場合、ほとんどの実装は型パニングでユニオンの使用をサポートします。ほとんどの実装(ただし、g ++ではありません)は、ポインターキャストがコンパイラーにはっきりと見える場合(ポインターキャストの不特定の定義について)、ポインターキャストもサポートします。そして、基礎となるハードウェアの「標準化」とは、次のようなことを意味します。

int
getExponent( double d )
{
    return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}

実際にはかなりポータブルです。 (もちろん、メインフレームでは動作しません。)動作しないのは、コンパイラからエイリアスが見えない最初の例のようなものです。 (これは標準の欠陥であると確信しています。それに関するDRを見たことさえ覚えているようです。)

3
James Kanze