あなたが遭遇するCの一般的な未定義/未指定の動作は何ですか?

言語弁護士の質問です。うーん。

個人的なトップ 3:

<オール> <リ>

厳密なエイリアシング規則に違反しています

<リ>

厳密なエイリアシング規則に違反しています

<リ>

厳密なエイリアシング規則に違反しています

:-)

編集 これは、2 回間違っている小さな例です:

(32 ビット整数とリトル エンディアンを想定)

float funky_float_abs (float a)
{
  unsigned int temp = *(unsigned int *)&a;
  temp &= 0x7fffffff;
  return *(float *)&temp;
}

このコードは、float の表現で符号ビットを直接ビット操作して、float の絶対値を取得しようとします。

ただし、ある型から別の型にキャストしてオブジェクトへのポインターを作成した結果は、有効な C ではありません。コンパイラーは、異なる型へのポインターが同じメモリーのチャンクを指していないと想定する場合があります。これは、void* と char* を除くすべての種類のポインターに当てはまります (符号は関係ありません)。

上記の場合、私はそれを2回行います。 1 回は float a の int-alias を取得し、1 回は値を float に変換します。

同じことを行う有効な方法が 3 つあります。

キャスト中に char または void ポインターを使用します。これらは常に何にでもエイリアスされるため、安全です。

float funky_float_abs (float a)
{
  float temp_float = a;
  // valid, because it's a char pointer. These are special.
  unsigned char * temp = (unsigned char *)&temp_float;
  temp[3] &= 0x7f;
  return temp_float;
}

メモリコピーを使用します。 Memcpy は void ポインターを受け取るため、エイリアシングも強制されます。

float funky_float_abs (float a)
{
  int i;
  float result;
  memcpy (&i, &a, sizeof (int));
  i &= 0x7fffffff;
  memcpy (&result, &i, sizeof (int));
  return result;
}

3 番目の有効な方法:ユニオンを使用します。これは明示的に C99 以降未定義ではありません:

float funky_float_abs (float a)
{
  union 
  {
     unsigned int i;
     float f;
  } cast_helper;

  cast_helper.f = a;
  cast_helper.i &= 0x7fffffff;
  return cast_helper.f;
}

私の個人的なお気に入りの未定義の動作は、空でないソース ファイルが改行で終わらない場合、動作が未定義になるというものです。

警告を発する以外に、ソースファイルが改行で終了しているかどうかに応じて、ソースファイルを異なる方法で処理したコンパイラは今まで見たことがありませんが、それは本当だと思います。したがって、この警告に驚くかもしれないということを除けば、知らないプログラマーを驚かせることはありません。

したがって、真の移植性の問題 (ほとんどが未指定または未定義ではなく実装に依存しますが、それは質問の精神に当てはまると思います):

  • char は必ずしも (un)signed であるとは限りません。
  • int は 16 ビット以上の任意のサイズです。
  • float は必ずしも IEEE 形式または準拠しているわけではありません。
  • 整数型は必ずしも 2 の補数であるとは限りません。また、整数演算のオーバーフローにより、未定義の動作が発生します (最新のハードウェアはクラッシュしませんが、一部のコンパイラの最適化により、ハードウェアが行うことであってもラップアラウンドとは異なる動作が発生します。たとえば、if (x+1 < x) x の場合は常に false として最適化される可能性があります 署名された型があります:-fstrict-overflow を参照してください オプション)。
  • "/", "." #include 内の「..」と「..」には定義された意味がなく、コンパイラによって扱いが異なる可能性があります (これは実際には異なります。問題が発生すると、1 日が台無しになります)。

動作が部分的に未定義/未指定であるため、開発したプラットフォームでさえ驚くことができる本当に深刻なもの:

    <リ>

    POSIX スレッド化と ANSI メモリ モデル。メモリへの同時アクセスは、初心者が考えるほど明確に定義されていません。 volatile は、初心者が考えることをしません。メモリ アクセスの順序は、初心者が考えるほど明確に定義されていません。アクセス できる 特定の方向にメモリバリアを越えて移動します。メモリ キャッシュの一貫性は必要ありません。

    <リ>

    コードのプロファイリングは、思ったほど簡単ではありません。テスト ループが効果がない場合、コンパイラはその一部または全部を削除できます。 inline には定義された効果はありません。

そして、ついでにニルスが言及したように:

  • 厳格なエイリアシング規則に違反しています。

何かへのポインタで何かを割ること。何らかの理由でコンパイルされません... :-)

result = x/*y;