web-dev-qa-db-ja.com

型付きポインタの逆参照は、厳密なエイリアス規則を破ります

次のコードを使用して、大きなプログラムの一部としてファイルからデータを読み取りました。

double data_read(FILE *stream,int code) {
        char data[8];
        switch(code) {
        case 0x08:
            return (unsigned char)fgetc(stream);
        case 0x09:
            return (signed char)fgetc(stream);
        case 0x0b:
            data[1] = fgetc(stream);
            data[0] = fgetc(stream);
            return *(short*)data;
        case 0x0c:
            for(int i=3;i>=0;i--)
                data[i] = fgetc(stream);
            return *(int*)data;
        case 0x0d:
            for(int i=3;i>=0;i--)
                data[i] = fgetc(stream);
            return *(float*)data;
        case 0x0e:
            for(int i=7;i>=0;i--)
                data[i] = fgetc(stream);
            return *(double*)data;
        }
        die("data read failed");
        return 1;
    }

今、私は-O2と次のgcc警告が表示されます:warning: dereferencing type-punned pointer will break strict-aliasing rules

Googleing私は2つの直交する答えを見つけました:

最後に、警告を無視したくありません。あなたは何をお勧めします?

[update]おもちゃの例を実際の関数に置き換えました。

45
Framester

本当にfreadを使いたいかのように見えます:

int data;
fread(&data, sizeof(data), 1, stream);

とはいえ、文字を読み込むルートに進み、intとして再解釈したい場合、Cでそれを行う安全な方法(ただし、C++ではnot)はユニオンを使用することです:

union
{
    char theChars[4];
    int theInt;
} myunion;

for(int i=0; i<4; i++)
    myunion.theChars[i] = fgetc(stream);
return myunion.theInt;

元のコードのdataの長さが3である理由がわかりません。4バイトが必要だと思います。少なくとも、intが3バイトのシステムは知りません。

あなたのコードと私のコードの両方は非常に移植性がないことに注意してください。

編集:ファイルからさまざまな長さの整数を読みたい場合は、次のようにしてください:

unsigned result=0;
for(int i=0; i<4; i++)
    result = (result << 8) | fgetc(stream);

(注:実際のプログラムでは、EOFに対してfgetc()の戻り値をさらにテストする必要があります。)

これは、システムのエンディアンが何であるかに関わらず、リトルエンディアン形式のファイルから4バイトの符号なしに関係なく読み取ります。これは、符号なしが少なくとも4バイトであるすべてのシステムで動作するはずです。

エンディアンに中立になりたい場合は、ポインターやユニオンを使用しないでください。代わりにビットシフトを使用してください。

26
Martin B

この問題は、_double*_を介してchar-arrayにアクセスするために発生します。

_char data[8];
...
return *(double*)data;
_

しかし、gccは、異なるタイプのポインターを介してプログラムが変数にアクセスしないことを前提としています。この仮定はストリクトエイリアスと呼ばれ、コンパイラがいくつかの最適化を行えるようにします。

コンパイラが*(double*)が_data[]_とオーバーラップできないことを知っている場合、コードの並べ替えなど、あらゆる種類のものを許可します。

_return *(double*)data;
for(int i=7;i>=0;i--)
    data[i] = fgetc(stream);
_

ループはほとんどの場合最適化されており、最終的には次のようになります。

_return *(double*)data;
_

Data []は初期化されません。この特定のケースでは、コンパイラーはポインターがオーバーラップしていることを確認できるかもしれませんが、_char* data_を宣言した場合は、バグを与える可能性があります。

ただし、ストリクトエイリアスルールでは、char *とvoid *は任意の型を指すことができるとされています。したがって、次のように書き換えることができます。

_double data;
...
*(((char*)&data) + i) = fgetc(stream);
...
return data;
_

厳密なエイリアス警告は、理解または修正するために非常に重要です。それらは、特定のマシンの特定のオペレーティングシステムの特定のコンパイラでのみ、満月や年に1回などで発生するため、社内で再現できない種類のバグを引き起こします。

39
Lasse Reinhold

このドキュメントは状況を要約しています: http://dbp-consulting.com/tutorials/StrictAliasing.html

いくつかの異なるソリューションがありますが、最も移植性が高く安全なのはmemcpy()を使用することです。 (関数呼び出しは最適化されている可能性があるため、表示されるほど効率的ではありません。)たとえば、これを置き換えます:

return *(short*)data;

これとともに:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
7
Thatcher Ulrich

ユニオンの使用はnotここで行う正しいことです。書かれていない共用体のメンバーからの読み取りは未定義です。つまり、コンパイラーは、コードを破壊する最適化を自由に実行できます(書き込みを最適化するなど)。

7
anon

基本的には、gccのメッセージをトラブルを探している人、警告しないと言わないでくださいと読むことができます。

3バイトの文字配列をintにキャストすることは、これまで見た中で最悪のことの1つです。通常、intには少なくとも4バイトがあります。したがって、4番目(intがより広い場合はさらに多く)で、ランダムデータが取得されます。そして、これらすべてをdoubleにキャストします。

どれもしません。 gccが警告するエイリアシングの問題は、あなたがやっていることと比較して無害です。

2
Jens Gustedt

C規格の作成者は、理論的には可能だが、一見無関係なポインタを使用してグローバル変数の値にアクセスする可能性が低い状況で、コンパイラライターに効率的なコードを生成させたいと考えました。アイデアは、単一の式でポインターをキャストおよび逆参照することで型のパニングを禁止するのではなく、次のようなものを与えて言うことでした:

int x;
int foo(double *d)
{
  x++;
  *d=1234;
  return x;
}

コンパイラは、* dへの書き込みがxに影響しないと想定する権利があります。標準の作成者は、未知のソースからポインタを受け取った上記のような関数が、タイプが完全に一致することを要求せずに、一見無関係なグローバルをエイリアスする可能性があると仮定しなければならない状況をリストしたいと考えました。残念ながら、この理論的根拠は、コンパイラのそれ以外の場合はエイリアスが発生する可能性があると信じる理由がないの場合に、規格の作成者が最小適合の規格を記述することを意図していることを強く示唆していますが、ルールはそれを要求していませんコンパイラはエイリアシングを認識します明らかな場合 gccの作者は、標準の不完全に記述された言語に準拠しながら、できる限り最小のプログラムを生成することを決定しました。実際には便利であり、明らかな場合にエイリアスを認識する代わりに(エイリアスのように見えないものはそうではないと仮定することはできますが)、プログラマはmemcpy、したがって、未知のOriginのポインターがほぼすべてのエイリアスを生成する可能性を考慮して、最適化を妨げるコンパイラーを必要とします。

0
supercat