非アクティブなユニオンメンバーと未定義の動作にアクセスしていますか?


129

union最後の1セット以外のメンバーにアクセスするのはUB であるという印象を受けましたが、確実な参照が見つからないようです(UBであると主張するが、標準からのサポートがないという回答以外)。

それで、それは未定義の振る舞いですか?


3
C99(およびC ++ 11も信じています)は、共用体を使用した型抜きを明示的に許可しています。したがって、「実装定義」の動作に該当すると思います。
ミスティシャル

1
個別のintからcharに変換するために何度か使用しました。それで、私はそれが未定義ではないことを確かに知っています。Sun CCコンパイラで使用しました。そのため、コンパイラに依存している可能性があります。
go4sri

42
@ go4sri:明らかに、動作が未定義であることの意味がわかりません。それがいくつかのインスタンスであなたのために働くように見えたという事実は、その未定義と矛盾していません。
ベンジャミンリンドリー


4
@Mysticial、あなたがリンクしているブログ投稿はC99に関して非常に具体的です。この質問はC ++のみにタグ付けされています。
davmac、2013

回答:


131

混乱は、Cが共用体を介した型パンニングを明示的に許可するのに対し、C ++()にはそのような許可はありません。

6.5.2.3構造と組合員

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

C ++の状況:

9.5ユニオン[class.union]

ユニオンでは、最大で1つの非静的データメンバーをいつでもアクティブにできます。つまり、最大で1つの非静的データメンバーの値をいつでもユニオンに格納できます。

C ++にはstruct、共通の初期シーケンスを持つsを含む共用体の使用を許可する言語が後であります。ただし、これは型パンニングを許可しません。

C ++で共用体型パンニング許可されているかどうかを判断するに、さらに検索する必要があります。それを思い出します は、C ++ 11の規範的な参照です(C99はC11と同様の言語で、共用体型パンニングを許可しています)。

3.9タイプ[basic.types]

4-T型のオブジェクトのオブジェクト表現は、T型のオブジェクトによって取り込まれたN個のunsigned charオブジェクトのシーケンスであり、Nはsizeof(T)に等しい。オブジェクトの値表現は、T型の値を保持するビットのセットです。自明なコピーが可能なタイプの場合、値表現は、値を決定するオブジェクト表現のビットのセットです。これは、実装の1つの個別の要素です。定義された値のセット。42
42)C ++のメモリモデルはISO / IEC 9899プログラミング言語Cのメモリモデルと互換性があるという意図があります。

読むと特に面白くなる

3.8オブジェクトの有効期間[basic.life]

タイプTのオブジェクトの存続期間は、次の場合に始まります。—タイプTの適切な配置とサイズのストレージが取得された場合—オブジェクトに重要な初期化がある場合、その初期化は完了です。

したがって、ユニオンに含まれるプリミティブ型(これはipso factoで簡単な初期化が行われます)の場合、オブジェクトの存続期間は少なくともユニオン自体の存続期間を含みます。これにより、

3.9.2複合型[basic.compound]

タイプTのオブジェクトがアドレスAにある場合、値がどのように取得されたかに関係なく、値がアドレスAであるタイプcv T *のポインターはそのオブジェクトを指すと言われます。

私たちが関心を持っている操作が型パンニング、つまり非アクティブなユニオンメンバーの値を取ると仮定し、上記のように、そのメンバーによって参照されるオブジェクトへの有効な参照があるとすると、その操作は左辺値-rvalue変換:

4.1左辺値から右辺値への変換[conv.lval]

非関数、非配列型Tのglvalueは、prvalueに変換できます。Tが不完全な型である場合、この変換を必要とするプログラムの形式は正しくありません。glvalueが参照するオブジェクトがタイプのオブジェクトでTはなく、から派生したタイプTのオブジェクトでもない場合、またはオブジェクトが初期化されていない場合、この変換を必要とするプログラムは未定義の動作をします。

次に問題は、非アクティブな共用体メンバーであるオブジェクトが、ストレージによってアクティブな共用体メンバーに初期化されるかどうかです。私の知る限り、これは事実ではありません。

  • 共用体がchar配列ストレージにコピーされて戻されます(3.9:2)、または
  • 共用体が同じタイプの別の共用体(3.9:3)にバイト単位でコピーされる、または
  • ユニオンは、ISO / IEC 9899(それが定義されている限り)(3.9:4注記42)に準拠するプログラム要素によって言語の境界を越えてアクセスされます。

非アクティブなメンバによって組合へのアクセスが定義されているオブジェクトと値表現に追従するように定義され、上記interpositionsの一つせずにアクセスが未定義の動作です。もちろん、実装では未定義の動作は発生しないと想定しているため、これはそのようなプログラムで実行できる最適化に影響を与えます。

つまり、非アクティブなユニオンメンバーに合法的に左辺値を形成できます(そのため、構築せずに非アクティブなメンバーに割り当てることはできます)、初期化されていないと見なされます。


5
3.8 / 1は、オブジェクトの存続期間は、そのストレージが再利用されるときに終了すると述べています。これは、ストレージがアクティブなメンバーに再利用されたため、ユニオンのライフタイムの非アクティブなメンバーが終了したことを示しています。これは、メンバーの使用方法に制限があることを意味します(3.8 / 6)。
bames53 2012年

2
その解釈の下では、メモリのすべてのビットに、単純に初期化可能で適切なアラインメントを持つすべてのタイプのオブジェクトが同時に含まれます...したがって、他のすべてのタイプでストレージが再利用されるため、重要でない初期化可能タイプの寿命はすぐに終了します(それらは自明に初期化可能ではないため、再起動しません)?
bames53 2012年

3
4.1という文言は完全に完全に破られており、それ以来書き直されています。それは完全に有効なあらゆる種類のものを許可しない:それはカスタム禁止memcpy(使用してオブジェクトにアクセスする実装をunsigned char左辺値)を、それはへのアクセスを禁止*pした後int *p = 0; const int *const *pp = &p;(からの暗黙的な変換にもかかわらずint**にはconst int*const*有効である)、それもアクセス禁止cstruct S s; const S &c = s;CWG問題616。新しい表現はそれを可能にしますか?[basic.lval]もあります。

2
@Omnifarious:それは理にかなっていますが&、ユニオンメンバーに適用した場合の単項演算子の意味も明確にする必要があります(C標準も明確にする必要があります)。結果として得られるポインターは、少なくとも他のメンバーの左辺値の次の直接または間接的な使用まで、メンバーにアクセスするために使用できるはずだと思いますが、gccでは、ポインターはそれ以上使用できないため、何が問題になるのでしょうか。&オペレータが意味することになっています。
スーパーキャット2017

4
「c99はC ++ 11の規範的な参照であることを思い出してください」に関する1つの質問は、c ++標準が明示的にC標準を参照している場合(たとえば、cライブラリ関数の場合)だけではありませんか?
MikeMB '15

28

C ++ 11標準はこのように述べています

9.5労働組合

ユニオンでは、最大で1つの非静的データメンバーをいつでもアクティブにできます。つまり、最大で1つの非静的データメンバーの値をいつでもユニオンに格納できます。

1つの値のみが格納されている場合、別の値をどのように読み取ることができますか?それだけではありません。


gccのドキュメントには、これが実装定義の動作にリストされています

  • 共用体オブジェクトのメンバーは、異なるタイプのメンバーを使用してアクセスされます(C90 6.3.2.3)。

オブジェクトの表現の関連するバイトは、アクセスに使用されるタイプのオブジェクトとして扱われます。タイプパニングを参照してください。これはトラップ表現である可能性があります。

これは、C標準ではこれが必須ではないことを示しています。


2016-01-05:コメントを通じて、C99欠陥レポート#283にリンクされました。これにより、類似のテキストが脚注としてC標準ドキュメントに追加されます。

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

しかし、脚注が標準の規範ではないことを考えると、それが多くを明確にするかどうかはわかりません。


10
@LuchianGrigore:UBは標準がUBと言うものではなく、標準がどのように機能するかを記述していないものです。これはまさにそのような場合です。規格は何が起こるかを説明していますか?実装が定義されていると言っていますか?いやいや。だからUBです。さらに、「メンバーが同じメモリアドレスを共有する」という引数に関しては、エイリアスルールを参照する必要があります。これにより、UBに再びアクセスできるようになります。
Yakov Galka

5
@Luchian:アクティブが何を意味するかは明確です。つまり、非静的データメンバーの最大1つの値はいつでも共用体に格納できます。
ベンジャミンリンドリー

5
@LuchianGrigore:はい、あります。規格が対応していない(できない)ケースは無限にあります。(C ++はTuringの完全なVMなので、不完全です。)では、何ですか?それは「アクティブ」の意味を説明しています。「それは」の後に上記の引用を参照してください。
Yakov Galka

8
@LuchianGrigore:定義セクションによると、動作の明示的な定義の省略も、未定義の動作と見なされます。
jxh

5
@Claudiuそれは別の理由でUBです-厳密なエイリアシングに違反しています。
ミスティシャル

18

標準が未定義の動作であると言うのに最も近いのは、共通の初期シーケンスを含む共用体の動作を定義するところです(C99、§6.5.2.3/ 5):

ユニオンの使用を簡略化するために、特別な保証が1つあります。ユニオンに共通の初期シーケンスを共有する複数の構造が含まれている場合(以下を参照)、ユニオンオブジェクトに現在これらの構造の1つが含まれている場合、共通の検査が許可されます。ユニオンの完全な型の宣言が表示される任意の場所のそれらの最初の部分。対応するメンバーが1つ以上の初期メンバーのシーケンスに対して互換性のあるタイプ(ビットフィールドの場合は同じ幅)を持っている場合、2つの構造は共通の初期シーケンスを共有します。

C ++ 11は、§9.2/ 19で同様の要件/許可を提供します。

標準レイアウト共用体に共通の初期シーケンスを共有する2つ以上の標準レイアウト構造体が含まれている場合、および標準レイアウト共用体オブジェクトにこれらの標準レイアウト構造体の1つが現在含まれている場合、任意の共通初期部分を検査できますそのうちの。2つの標準レイアウト構造体は、対応するメンバーにレイアウト互換のタイプがあり、どちらのメンバーもビットフィールドではないか、1つ以上の初期メンバーのシーケンスに対して同じ幅のビットフィールドである場合、共通の初期シーケンスを共有します。

どちらも直接は述べていませんが、これらはどちらもメンバーの「検査」(読み取り)が「許可されている」という強い意味合いを持っています 、1)メンバーが(一部)最近作成されたメンバーであるか、2)共通のイニシャルの一部である場合にのみシーケンス。

それは、それ以外の場合は未定義の動作であるという直接の声明ではありませんが、私が認識している最も近いものです。


これは完全にするために、あなたは「レイアウト互換タイプは、」C ++のためのものであるか、「互換性のあるタイプは」C.のためのものであるかを知る必要があり
マイケル・アンダーソン

2
@MichaelAnderson:はい、いいえ。何かがこの例外に該当するかどうかを確認する必要がある場合に、それらに対処する必要があります。しかし、ここでの本当の問題は、明らかに例外の外側にあるものが本当にUBを与えるかどうかです。これは、意図を明確にするために十分に暗示されていると思いますが、直接述べられたことはないと思います。
ジェリー・コフィン

この「一般的な初期シーケンス」のことで、私のプロジェクトの2つまたは3つがRewrite Binから保存された可能性があります。union特定のブログから大丈夫だとの印象を受け、いくつかの大きな構造とプロジェクトを構築したため、sのほとんどのパンニングの使用法が未定義であることを最初に読んだとき、私は素直でした。今、私は考えて私のために、私はすべての後にOKかもしれないunionsが前面に同じタイプを持つクラス含まれていません
underscore_d

@JerryCoffin、あなたは私と同じ質問をほのめかしていたと思います。たとえば、a やa unionが含まれている場合はどうでしょう-この条件がここでも当てはまると思いますが、s のみを許可するように非常に慎重に表現されています。幸いにも、私は生のプリミティブの代わりにそれらをすでに使用していuint8_tclass Something { uint8_t myByte; [...] };struct
ます

@underscore_d:C標準は少なくとも次の質問に対応しています:「適切に変換された構造体オブジェクトへのポインターは、その初期メンバーを指します(または、そのメンバーがビットフィールドの場合は、それが存在するユニットを指します)。 、 およびその逆。"
Jerry Coffin 2015

12

利用可能な回答でまだ言及されていないものは、セクション6.2.5のパラグラフ21の脚注37です。

union型のオブジェクトには一度に1つのメンバーしか含めることができないため、集約型にはunion型が含まれないことに注意してください。

この要件は、メンバーを書き込んだり、別のメンバーを読み取ったりしてはならないことを明確に示しているようです。この場合、仕様の欠如による未定義の動作である可能性があります。


多くの実装では、ストレージ形式とレイアウトルールが文書化されています。このような仕様は、多くの場合、あるタイプのストレージの読み取りと別のタイプの書き込みの効果が、ポインターを使用して読み取りと書き込みを行う場合を除いて、コンパイラーが実際に定義されたストレージ形式を使用する必要がないというルールがない場合の影響を示唆します文字タイプの。
スーパーキャット2016

-3

これを例を使ってよく説明します。
次の共用体があると仮定します。

union A{
   int x;
   short y[2];
};

私はそれsizeof(int)が4 を与え、それsizeof(short)が2 を与えると思います。
あなたがunion A a = {10}それを書くとき、タイプAの新しいvarを作成し、それに値10を入れます。

あなたの記憶は次のようになります:(すべての組合員が同じ場所を取得することを忘れないでください)

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

ご覧のとおり、axの値は10、ay 1の値は10、ay [0]の値は0です。

今、これを行うとどうなりますか?

a.y[0] = 37;

私たちの記憶は次のようになります:

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

これにより、axの値が2424842(10進数)になります。

現在、ユニオンにfloatまたはdoubleが含まれている場合、正確な数値を格納する方法が原因で、メモリマップは混乱します。あなたがここで得ることができるより多くの情報。


18
:)これは私が尋ねたものではありません。私は内部で何が起こるか知っています。私はそれが機能することを知っています。それが規格にあるかどうか尋ねました。
Luchian Grigore
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.