web-dev-qa-db-ja.com

ポインターのキャスト中の配置について心配する必要がありますか?

私のプロジェクトには、次のようなコードがあります。

// raw data consists of 4 ints
unsigned char data[16];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + 4));
i3 = *((int*)(data + 8));
i4 = *((int*)(data + 12));

このコードはunsigned char*int*にキャストしようとしているため、通常はより厳密な配置要件があるため、このコードは移植できない可能性があると技術リーダーに話しました。しかし、技術リーダーはそれで大丈夫だと言っています。ほとんどのコンパイラーはキャスト後も同じポインター値のままであり、私はこのようなコードを書くことができます。

正直なところ、私は本当に確信していません。調査の結果、上記のようなポインタキャストの使用に反対する人がいます。たとえば、 herehere などです。

だからここに私の質問があります:

  1. 実際のプロジェクトでキャストした後、ポインターを逆参照しても本当に安全ですか?
  2. Cスタイルのキャストとreinterpret_castの間に違いはありますか?
  3. CとC++の間に違いはありますか?
51
Eric Z

1.実際のプロジェクトでキャストした後、ポインターを逆参照しても本当に安全ですか?

ポインタが適切に配置されていない場合は、実際に問題が発生する可能性があります。私は実際に、_char*_をより厳密に整列された型にキャストすることによって引き起こされた実際の製品コードでバスエラーを確認して修正しました。明らかなエラーが発生しなくても、パフォーマンスの低下など、あまり明白でない問題が発生する可能性があります。すぐに問題が発生しなくても、UBを回避するために標準に厳密に従うことは良い考えです。 (そして、コードが違反している1つのルールは、厳密なエイリアスルール、§3.10/10 *です)

より良い代替策は、バッファがオーバーラップしている場合はstd::memcpy()または_std::memmove_を使用することです(またはより良い bit_cast<>()

_unsigned char data[16];
int i1, i2, i3, i4;
std::memcpy(&i1, data     , sizeof(int));
std::memcpy(&i2, data +  4, sizeof(int));
std::memcpy(&i3, data +  8, sizeof(int));
std::memcpy(&i4, data + 12, sizeof(int));
_

一部のコンパイラーは、プログラマーがしばしばこれを間違っているため、char配列が必要以上に厳密に整列されるようにするために、他のコンパイラーよりも強く働きます。

_#include <cstdint>
#include <typeinfo>
#include <iostream>

template<typename T> void check_aligned(void *p) {
    std::cout << p << " is " <<
      (0==(reinterpret_cast<std::intptr_t>(p) % alignof(T))?"":"NOT ") <<
      "aligned for the type " << typeid(T).name() << '\n';
}

void foo1() {
    char a;
    char b[sizeof (int)];
    check_aligned<int>(b); // unaligned in clang
}

struct S {
    char a;
    char b[sizeof(int)];
};

void foo2() {
    S s;
    check_aligned<int>(s.b); // unaligned in clang and msvc
}

S s;

void foo3() {
    check_aligned<int>(s.b); // unaligned in clang, msvc, and gcc
}

int main() {
    foo1();
    foo2();
    foo3();
}
_

http://ideone.com/FFWCjf

2. Cスタイルのキャストとreinterpret_castの間に違いはありますか?

場合によります。 Cスタイルのキャストは、関係するタイプに応じてさまざまなことを行います。ポインター型間のCスタイルのキャストは、reinterpret_castと同じ結果になります。 §5.4明示的な型変換(キャスト表記)および§5.2.9-11を参照してください。

3. CとC++の間に違いはありますか?

Cで有効な型を扱っている限り、それはありません。


*もう1つの問題は、C++が、1つのポインタ型から、より厳密な配置要件を持つ型へのキャストの結果を指定しないことです。これは、位置合わせされていないポインタを表現することさえできないプラットフォームをサポートするためです。ただし、今日の典型的なプラットフォームは、境界整列されていないポインターを表すことができ、コンパイラーはそのようなキャストの結果を期待どおりに指定します。そのため、この問題はエイリアス違反の二次的なものです。 [expr.reinterpret.cast]/7を参照してください。

35
bames53

本当に大丈夫じゃない。配置が間違っている可能性があり、コードが厳密なエイリアスに違反している可能性があります。明示的に解凍する必要があります。

i1 = data[0] | data[1] << 8 | data[2] << 16 | data[3] << 24;

これは間違いなく明確に定義された動作であり、ボーナスとして、ポインタキャストとは異なり、エンディアンに依存しません。

27
Puppy

ここで示している例では、初期charポインターが正しく配置されていれば、ほとんどすべての最新のCPUで安全に実行できます。一般に、これは安全ではなく、動作することが保証されていません。

最初のcharポインターが正しく位置合わせされていない場合、これはx86およびx86_64で機能しますが、他のアーキテクチャーでは失敗する可能性があります。運が良ければクラッシュするだけなので、コードを修正します。運が悪い場合は、非整列アクセスがオペレーティングシステムのトラップハンドラーによって修正され、なぜ遅いのかについての明確なフィードバックがなくてもひどいパフォーマンスが得られます(一部のコードでは氷河的にゆっくり話していますが、これはアルファの20年前の大きな問題でした)。

X86&coでも、非境界整列アクセスは遅くなります。

現在も将来も安全を確保したい場合は、このような割り当てを行う代わりに、memcpyを使用してください。最近のコンパイラーはmemcpyを最適化して正しいことを行う可能性が高く、そうでない場合はmemcpy自体がアライメント検出を行い、最速の処理を行います。

また、あなたの例は1つの点で間違っています:sizeof(int)は常に4であるとは限りません。

6
Art

charバッファーデータをアンパックする正しい方法は、memcpyを使用することです。

unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
memcpy(&i1, data, sizeof(int));
memcpy(&i2, data + sizeof(int), sizeof(int));
memcpy(&i3, data + 2 * sizeof(int), sizeof(int));
memcpy(&i4, data + 3 * sizeof(int), sizeof(int));

キャストはエイリアスに違反します。つまり、コンパイラとオプティマイザはソースオブジェクトを初期化されていないものとして自由に扱うことができます。

3つの質問について:

  1. いいえ、エイリアスと配置のため、キャストポインターの逆参照は一般に安全ではありません。
  2. いいえ、C++では、Cスタイルのキャストはreinterpret_castによって定義されます。
  3. いいえ、CとC++はキャストベースのエイリアシングに同意します。ユニオンベースのエイリアシングの扱いには違いがあります(Cでは可能ですが、C++ではできません)。
4
ecatmur

更新:私は、実際の例のように、実際には小さい型が大きい型に比べて整列されていない可能性があるという事実を見落としました。配列をキャストする方法を逆にすることでその問題を軽減できます。配列をintの配列として宣言し、その方法でアクセスする必要がある場合は_char *_にキャストします。

_// raw data consists of 4 ints
int data[4];

// here's the char * to the original data
char *cdata = (char *)data;
// now we can recast it safely to int *
i1 = *((int*)cdata);
i2 = *((int*)(cdata + sizeof(int)));
i3 = *((int*)(cdata + sizeof(int) * 2));
i4 = *((int*)(cdata + sizeof(int) * 3));
_

プリミティブ型の配列には何の問題もありません。配列の問題は 構造化データ (Cのstruct)の配列を処理するときに発生します配列の元の特権型がキャストされたものより大きい場合to、上記の更新を参照してください。

Charの配列をintの配列にキャストしても、コードが実行されるはずのプラットフォームのintのサイズに一致するように、sizeof(int)でオフセットを置き換えれば、まったく問題ありません。

_// raw data consists of 4 ints
unsigned char data[4 * sizeof(int)];
int i1, i2, i3, i4;
i1 = *((int*)data);
i2 = *((int*)(data + sizeof(int)));
i3 = *((int*)(data + sizeof(int) * 2));
i4 = *((int*)(data + sizeof(int) * 3));
_

endianness の問題が発生するのは、そのデータをバイト順序が異なるプラットフォーム間で共有した場合のみであることに注意してください。それ以外の場合は、完全に問題ないはずです。

1
didierc

コンパイラのバージョンに応じて状況がどのように異なるかを彼に示すことができます。

アラインメントとは別に、2番目の問題があります。標準では、int*char*にキャストできますが、逆はできません(char*int*から最初にキャストされた場合を除きます)。 詳細については、この投稿を参照してください。

1
StackedCrooked

アラインメントについて心配する必要があるかどうかは、ポインタの発生元であるオブジェクトのアラインメントによって異なります。

より厳密な配置要件を持つ型にキャストすると、移植性がなくなります。

char配列のベースは、例のように、エレメントタイプcharの場合よりも厳密な配置である必要はありません。

ただし、任意のオブジェクト型へのポインターは、配置に関係なく、char *に変換して戻すことができます。 char *ポインターは、元のより強い配置を保持します。

Unionを使用して、より強く整列されたchar配列を作成できます。

union u {
    long dummy; /* not used */
    char a[sizeof(long)];
};

ユニオンのすべてのメンバーは同じアドレスで始まります。最初にパディングはありません。したがって、ユニオンオブジェクトがストレージで定義されている場合、最も厳密に整列されたメンバーに適した整列が必要です。

上記のunion uは、long型のオブジェクトに対して厳密に配置されています。

アラインメントの制限に違反すると、一部のアーキテクチャに移植したときにプログラムがクラッシュする可能性があります。または、機能する場合がありますが、パフォーマンスの影響が軽度から重度ですが、メモリアライメントの不整合がハードウェアで実装されているか(追加のサイクルがいくらかかかります)、ソフトウェアで実装されています(ソフトウェアがアクセスをエミュレートするカーネルへのトラップを犠牲にして)。多くのサイクルの)。

0
Kaz