厳密なエイリアスルールが間違って指定されていませんか? 質問する

厳密なエイリアスルールが間違って指定されていませんか? 質問する

として以前に確立された、形式の和集合

union some_union {
    type_a member_a;
    type_b member_b;
    ...
};

メンバーは重複するストレージ内の + 1 つのオブジェクト: ユニオン自体に 1 つのオブジェクト、および各ユニオン メンバーに 1 つのオブジェクト。最後に書き込まれたユニオン メンバーではないものを読み取る場合でも、任意のユニオン メンバーに対して任意の順序で自由に読み取りおよび書き込みできることは明らかです。ストレージにアクセスするための lvalue には正しい有効な型があるため、厳密なエイリアシング ルールに違反することはありません。

これはさらにサポート脚注 95 では、型のパンニングが共用体の意図された使用法である理由が説明されています。

によって可能になる最適化の典型的な例は、厳格なエイリアシングルールこの関数は次のようになります:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

コンパイラはこれを次のように最適化するかもしれない。

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (1);
}

への書き込みが*fの値に影響を与えないと安全に想定できるためです*i

しかし、同じ共用体のメンバーに 2 つのポインターを渡すとどうなるでしょうか。 がfloatIEEE 754 単精度浮動小数点数で、 がint32 ビットの 2 の補数整数である一般的なプラットフォームを想定して、次の例を考えてみましょう。

int breaking_example(void)
{
    union {
        int i;
        float f;
    } fi;

    return (strict_aliasing_example(&fi.i, &fi.f));
}

前述のとおり、 およびfi.ifi.f重複するメモリ領域を参照します。それらの読み取りと書き込みは、順序に関係なく無条件に合法です (書き込みは、共用体が初期化された後にのみ合法です)。私の意見では、すべての主要コンパイラによって実行される前述の最適化では、異なるタイプの 2 つのポインタが同じ場所を合法的に指すため、誤ったコードが生成されます。

どういうわけか、厳密なエイリアシング ルールの解釈が正しいとは信じられません。厳密なエイリアシングが設計された最適化そのものが、前述のコーナー ケースのせいで不可能であるというのは、もっともらしく思えません。

なぜ私が間違っているのか教えてください。

関連する質問調査中に発見されました。

自分の回答を追加する前に、既存の回答とそのコメントをすべて読んで、自分の回答が新しい議論を追加するものであることを確認してください。

ベストアンサー1

あなたの例から始めましょう:

int strict_aliasing_example(int *i, float *f)
{
    *i = 1;
    *f = 1.0;
    return (*i);
}

まず、共用体がない場合、 と が両方とも同じオブジェクトを指している場合、これは厳密なエイリアシング ルールに違反することを認識しましょうifオブジェクトに宣言された型がないと仮定すると、 は*i = 1有効な型を に設定しint*f = 1.0次に に設定しfloat、最後に は型の左辺値return (*i)を介して の有効な型を持つオブジェクトにアクセスしますが、これは明らかに許可されていません。floatint

問題は、 と の両方if同じ共用体のメンバーを指している場合、これが依然として厳密なエイリアシング違反になるかどうかです。 これに該当しない場合は、この状況に適用される厳密なエイリアシング規則から何らかの特別な例外が存在するか、 を介してオブジェクトにアクセスしても と*i同じオブジェクトに(また)アクセスしないかのいずれかになります*f

「.」メンバー アクセス演算子による共用体メンバー アクセスについては、標準では次のように規定されています (6.5.2.3)。

. 演算子と識別子が続く接尾辞式は、構造体または共用体オブジェクトのメンバーを指定します。値は、名前付きメンバー (95) の値であり、最初の式が左辺値の場合は左辺値になります。

上記の脚注95には次のように記されている。

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

これは明らかにユニオンによる型パンニングを許可することを意図しているが、(1)脚注は非規範的であり、動作を禁止するものではなく、むしろ仕様の残りの部分に従ってテキストの一部の意図を明確にするべきであり、(2)ユニオンによる型パンニングのこの許可は、コンパイラベンダーによって適用されると見なされていることに注意する必要がある。組合員アクセスオペレータ経由のアクセスのみ- それ以外の場合、厳密なエイリアシングは最適化にはほとんど役に立ちません。ほぼすべての 2 つのポインターが同じ共用体の異なるメンバーを参照する可能性があるためです (あなたの例がその好例です)。

したがって、この時点で、次のことが言えます。

  • あなたの例のコードは、非規範的な脚注によって明示的に許可されています
  • 一方、規範的なテキストでは、ユニオンの1つのメンバーにアクセスすると別のメンバーへのアクセスも構成されると想定して、あなたの例を許可していないようです(厳密なエイリアシングのため)。ただし、これについては後ほど詳しく説明します。

しかし、ユニオンの 1 つのメンバーにアクセスすると、実際に他のメンバーにもアクセスするのでしょうか。そうでない場合、厳密なエイリアス ルールはこの例には関係ありません。(関係する場合、厳密なエイリアス ルールは、ユニオンを介したほぼすべての型パンニングを許可しないという問題があります)。

ユニオンは次のように定義されます(6.2.5 パラグラフ 20)。

ユニオン型は重複する空でないメンバーオブジェクトの集合を記述する。

また、次の点に注意してください (6.7.2.1 パラグラフ 16):

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

以来アクセス(3)は:

〈実行時アクション〉オブジェクトの値を読み取ったり変更したりする

...そして、非アクティブなユニオン メンバーには格納された値がないため、おそらく 1 つのメンバーにアクセスしても、他のメンバーへのアクセスにはならないと考えられます。

ただし、メンバー アクセスの定義 (上記引用の 6.5.2.3) には、「値は指定されたメンバーの値です」と記載されています (これは、脚注 95 が添付されている正確な記述です)。メンバーに値がない場合、どうなるのでしょうか。脚注 95 に回答が記載されていますが、前述したように、規範的なテキストではサポートされていません。

いずれにせよ、このテキストには、共用体メンバーを「メンバー オブジェクト経由で」(つまり、メンバー アクセス演算子を使用した式経由で直接) 読み取ったり変更したりすることが、同じメンバーへのポインタ経由で読み取ったり変更したりすることと異なるということを示唆するものは何もないように思われます。異なる型のポインタはエイリアスしないという前提で最適化を実行でき、型パンニングはメンバー アクセスを含む式経由でのみ実行する必要があるという、コンパイラ ベンダーによって適用されたコンセンサス理解は、標準のテキストではサポートされていません。

脚注 95 が規範的であると見なされる場合、テキストの残りの部分によると、あなたの例は未定義の動作のない完全に適切なコードです ( の値が(*i)トラップ表現でない限り)。ただし、脚注 95 が規範的ではないと見なされる場合、格納された値を持たないオブジェクトへのアクセスが試行され、その場合の動作はせいぜい不明瞭です (ただし、厳密なエイリアシング ルールはおそらく関係ありません)。

現在、コンパイラ ベンダーの理解では、この例には未定義の動作がありますが、これは標準で指定されていないため、コードがどの制約に違反しているかは正確にはわかりません。

個人的には、標準の「修正」は次のようになると考えています。

  • メンバー アクセス式の左辺値変換、または左側がメンバー アクセス式である割り当てを経由する場合を除き、非アクティブな共用体メンバーへのアクセスを禁止します (問題のメンバーが文字型である場合は、厳密なエイリアシング ルール自体に同様の例外があるため、最適化に影響がないため、例外が作成される場合があります)。
  • 非アクティブメンバーの価値は、現在脚注95で説明されているとおりであることを規範テキストに明記する

そうすると、あなたの例は厳密なエイリアシング規則に違反するのではなく、非アクティブな共用体メンバーにはメンバー アクセス演算子 (および適切なメンバー) を含む式を介してのみアクセスする必要があるという制約に違反することになります。

したがって、あなたの質問に答えるために-厳密なエイリアシングルールが間違って指定されていませんか?- いいえ、厳密なエイリアシング ルールはこの例には関係ありません。2 つのポインター逆参照によってアクセスされるオブジェクトは別々のオブジェクトであり、ストレージ内で重複していても、一度に値を持つのはそのうちの 1 つだけだからです。ただし、ユニオン メンバー アクセス ルールは正しく指定されていません。

欠陥レポート 236 に関する注記:

ユニオンセマンティクスに関する議論では、必ずDR236確かに、あなたのサンプルコードは、その欠陥レポートのコードと表面的には非常に似ています。次の点に注意してください。

  1. DR 236の例はない型パンニングについて。これは、そのメンバーへのポインタを介して非アクティブなユニオン メンバーに代入することが適切かどうかに関するものです。問題のコードは、2 番目のメンバーに書き込んだ後に「元の」ユニオン メンバーに再度アクセスしようとしないため、ここでの質問のコードとは微妙に異なります。したがって、サンプル コードの構造は類似しているものの、欠陥レポートは質問とはほとんど関係がありません。
  2. 「委員会は、例 2 が 6.5 パラグラフ 7 のエイリアシング ルールに違反していると考えています」 - これは、委員会が、ユニオン オブジェクトのメンバー アクセスを含む式を介さずに「非アクティブ」ユニオン メンバーを記述することは、厳密なエイリアシング違反であると考えていることを示しています。上で詳述したように、これは標準のテキストではサポートされていません。
  3. 「ルールに違反しないようにするには、例の関数 f は次のように記述する必要があります」 - つまり、アクティブなメンバー タイプを変更するには、ユニオン オブジェクト (および「.」演算子) を使用する必要があります。これは、私が上で提案した標準の「修正」と一致しています。
  4. DR 236 の委員会の回答では、「両方のプログラムが未定義の動作を呼び出す」と主張しています。最初のプログラムがなぜそうするのかについては説明されておらず、2 番目のプログラムがなぜそうするのかについても説明が間違っているようです。

おすすめ記事