厳密なエイリアスルールとは何ですか?


804

Cでの一般的な未定義の動作について尋ねるとき、人々は厳密なエイリアシング規則を参照することがあります。
彼らは何を話している?


12
@Ben Voigt:エイリアス規則はc ++とcでは異なります。なぜこの質問はとでタグ付けされcていc++faqます。
MikeMB 2015

6
@MikeMB:他の専門家が既存の回答の下から質問を変更しようとしたにもかかわらず、履歴を確認すると、タグを元の状態のままにしていたことがわかります。さらに、言語依存とバージョン依存は、「厳密なエイリアスルールとは何ですか?」への回答の非常に重要な部分です。また、CとC ++の間でコードを移行するチームや、両方で使用するマクロを作成するチームにとって、違いを知ることは重要です。
Ben Voigt 2015

6
@Ben Voigt:実際-私の知る限り-ほとんどの回答はc ++にではなくcにのみ関連します。また、質問の文言はCルールに焦点を合わせていることを示します(またはOPは違いを認識していませんでした。 )。ルールと一般的なアイデアの大部分はもちろん同じですが、特に、ユニオンが関係する場合、答えはC ++には適用されません。私は少し心配です、一部のc ++プログラマーは厳密なエイリアシングルールを探し、ここで述べられているすべてがc ++にも適用されると仮定します。
MikeMB 2015

一方、良い回答がたくさん投稿された後に質問を変更することは問題があり、問題はとにかく小さな問題であることに同意します。
MikeMB 2015

1
@MikeMB:受け入れられた回答にCが焦点を当てているため、C ++では正しくないため、サードパーティによって編集されたと思います。その部分はおそらく再度修正する必要があります。
Ben Voigt 2015

回答:


562

厳密なエイリアシングの問題が発生する一般的な状況は、(デバイスまたはネットワークメッセージのような)構造体をシステムのワードサイズのバッファ(uint32_tsまたはuint16_ts へのポインタのような)にオーバーレイするときです。このようなバッファーに構造体をオーバーレイするか、ポインターのキャストを介してそのような構造体にバッファーをオーバーレイすると、厳密なエイリアス規則に簡単に違反する可能性があります。

したがって、この種の設定で何かにメッセージを送信したい場合、同じメモリのチャンクを指す2つの互換性のないポインタが必要になります。それから私はこのようなものを単純にコーディングするかもしれません(を備えたシステム上でsizeof(int) == 2):

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;

void SendWord(uint32_t);

int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));

    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);

    // Send a bunch of messages    
    for (int i =0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

厳密なエイリアス規則により、この設定は無効になります。互換性のある型ではないオブジェクト、またはC 2011 6.5段落7 1で許可されている他の型の1つであるオブジェクトをエイリアスするポインターの逆参照は、未定義の動作です。残念ながら、あなたはまだ、この方法をコーディングすることができます多分あなたは、コードを実行したときにのみ、奇妙な予期しない動作を持っている、それは罰金をコンパイルする必要があり、いくつかの警告を取得します。

(GCCはエイリアシング警告を出す能力に多少の一貫性がないように見え、フレンドリーな警告を与えることもあるし、与えないこともあります。)

この動作が定義されていない理由を確認するには、厳密なエイリアシングルールがコンパイラに何を購入するかを考える必要があります。基本的に、このルールでbuffは、ループのすべての実行の内容を更新するために命令を挿入することを考える必要はありません。代わりに、最適化するときに、エイリアシングに関してうっとうしく強制されていない仮定を使用して、これらの命令を省略し、ループが実行される前に一度CPUレジスタにロードbuff[0]してbuff[1]、ループの本体を高速化できます。厳密なエイリアシングが導入される前は、コンパイラは内容がbuffいつでもどこからでもだれでも変更できるパラノイア状態に陥っていました。そのため、パフォーマンスをさらに向上させるために、そしてほとんどの人が型抜きのポインターを使用しないと想定して、厳密なエイリアス規則が導入されました。

例が不自然であると思う場合は、代わりにバッファを送信している別の関数にバッファを渡していても、これが発生する可能性があることに注意してください。

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

そして、この便利な機能を利用するために以前のループを書き直しました

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

コンパイラーは、SendMessageをインライン化しようとすることができるか、または十分にスマートであるとは限りません。また、buffを再度ロードするかしないかを決定する場合としない場合があります。SendMessage個別にコンパイルされた別のAPIの一部である場合、おそらくbuffのコンテンツをロードするための指示があります。次に、C ++を使用している可能性があります。これは、テンプレート化されたヘッダーのみの実装であり、コンパイラーはインライン化できると考えています。あるいは、自分の便宜のために.cファイルに書き込んだものかもしれません。とにかく、未定義の動作が引き続き発生する可能性があります。内部で何が起こっているかを知っていても、それは依然としてルール違反であるため、明確に定義された動作は保証されません。したがって、単語区切りバッファを使用する関数をラップするだけでは、必ずしも効果はありません。

どうすればこれを回避できますか?

  • ユニオンを使用します。ほとんどのコンパイラは、厳密なエイリアシングについて文句を言うことなくこれをサポートします。これはC99では許可されており、C11では明示的に許可されています。

    union {
        Msg msg;
        unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
    };
    
  • コンパイラで厳格なエイリアスを無効にすることができます(f [no-] strict-aliasing in gcc))

  • char*システムの単語の代わりにエイリアスに使用できます。ルールはchar*signed charおよびを含むunsigned char)の例外を許可します。は常にchar*他のタイプのエイリアスと見なされます。ただし、これは他の方法では機能しません。構造体が文字のバッファをエイリアスするという仮定はありません。

初心者は注意してください

これは、2つのタイプを互いにオーバーレイする場合の1つの潜在的な地雷原です。また、エンディアン単語の整列構造体を正しくパッキングして整列の問題に対処する方法についても学習する必要があります。

脚注

1 C 2011 6.5 7が左辺値にアクセスを許可するタイプは次のとおりです。

  • オブジェクトの有効なタイプと互換性のあるタイプ
  • オブジェクトの有効な型と互換性のある型の修飾バージョン
  • オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型
  • オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型
  • メンバーの中に前述のタイプの1つを含む集約タイプまたはユニオンタイプ(再帰的に、サブアグリゲートのメンバーまたは含まれるユニオンを含む)、または
  • 文字タイプ。

16
戦いの後で来ているようです。代わりにunsigned char*遠方で使用される可能性はありますchar*か?バイトが署名されておらず、署名された動作の奇妙さ(特にオーバーフローが発生しない)を望まないため、の基になる型としてではunsigned charなく使用する傾向がありますcharbyte
Matthieu M.

30
@Matthieu:署名がエイリアスルールに影響を与えないため、使用してunsigned char *も問題ありません。
Thomas Eding、2011年

22
最後に書き込まれたものとは異なるユニオンメンバーから読み取るのは未定義の動作ではないですか?
R.マルティーニョフェルナンデス

23
Bollocks、この答えは完全に逆です。違法であると示す例は実際に合法であり、合法であると示す例は実際に違法です。
R.マルティーニョフェルナンデス

7
あなたuint32_t* buff = malloc(sizeof(Msg));とそれに続くユニオンunsigned int asBuffer[sizeof(Msg)];バッファ宣言は異なるサイズを持ち、どちらも正しくありません。mallocコールは、それが必要以上に4倍大きくなります...私はそれを明確にするためであることを理解ボンネットの下に4バイトアライメント(それをしない)と労働組合に依存するが、それはバグ私をなし・ザされます少ない...
センシクル

233

私が見つけた最高の説明は、Mike Acton、Understanding Strict Aliasingです。PS3開発に少し焦点を当てていますが、それは基本的には単なるGCCです。

記事から:

「厳密なエイリアシングは、C(またはC ++)コンパイラによって作成された、異なる型のオブジェクトへのポインタの逆参照は、同じメモリ位置を参照しない(つまり、相互にエイリアシングする)ことを前提としています。」

だから、基本的にあなたが持っている場合はint*、いくつかのメモリへのポインティングを含むint、その後、あなたはポイントfloat*そのメモリとしてそれを使用するfloatルールを破ります。コードがこれを尊重しない場合、コンパイラのオプティマイザがコードを破壊する可能性が高くなります。

ルールの例外は、char*任意のタイプを指すことができるです。


6
では、2つの異なる型の変数で同じメモリを合法的に使用するための正規の方法は何ですか?または誰もが単にコピーしますか?
jiggunjer

4
Mike Actonのページには欠陥があります。少なくとも、「組合によるキャスティング(2)」の部分はまったく間違っています。彼が合法であると主張するコードはそうではありません。
davmac、2015

11
@davmac:C89の作者は、プログラマーにフープを飛び越えさせることを意図したことはありません。オプティマイザが冗長なコードを削除することを期待して、プログラマがデータを冗長的にコピーするコードを書くように要求するような方法で、最適化の唯一の目的のために存在するルールが解釈されるべきであるという考えを徹底的に奇妙に思います。
スーパーキャット2013年

1
@curiousguy:「組合を作ることはできない」?第1に、ユニオンの元々の主な目的は、エイリアシングにまったく関係していません。第2に、現代の言語仕様では、エイリアスに共用体を使用することを明示的に許可しています。コンパイラーは、共用体が使用されていることを認識し、状況を処理することは特別な方法です。
AnT

5
@curiousguy:誤り。第1に、ユニオンの背後にある元々の概念的な考え方は、特定のユニオンオブジェクトには「アクティブ」なメンバーオブジェクトが常に1つしかなく、他のオブジェクトは存在しないというものでした。そのため、あなたが信じているように「同じアドレスに異なるオブジェクト」はありません。第2に、誰もが話しているエイリアシング違反は、1つのオブジェクトに別のオブジェクトとしてアクセスすることであり、単に同じアドレスを持つ 2つのオブジェクトを持っていることではありません。タイプパンニングアクセスがない限り、問題はありません。それが最初のアイデアでした。その後、共用体を介した型抜きが許可されました。
AnT 2017年

133

これは、C ++ 03標準のセクション3.10にある厳密なエイリアスルールです(他の答えは良い説明を提供しますが、ルール自体は提供していません)。

プログラムが次のタイプのいずれか以外の左辺値を介してオブジェクトの格納された値にアクセスしようとした場合の動作は未定義です。

  • オブジェクトの動的タイプ
  • オブジェクトの動的タイプのcv修飾バージョン、
  • オブジェクトの動的型に対応する符号付きまたは符号なしの型である型
  • オブジェクトの動的タイプのcv修飾バージョンに対応する符号付きまたは符号なしタイプのタイプ
  • メンバーの中に前述のタイプの1つを含む集合体またはユニオンタイプ(再帰的に、サブアグリゲートまたは含まれるユニオンのメンバーを含む)、
  • オブジェクトの動的な型の(場合によってはcv修飾された)基本クラス型である型
  • A charまたはunsigned charタイプ。

C ++ 11およびC ++ 14の表現(変更を強調):

プログラムが次のタイプのいずれか以外のglvalueを介してオブジェクトの格納された値にアクセスしようとした場合の動作は未定義です。

  • オブジェクトの動的タイプ
  • オブジェクトの動的タイプのcv修飾バージョン、
  • オブジェクトの動的タイプに類似したタイプ(4.4で定義)
  • オブジェクトの動的型に対応する符号付きまたは符号なしの型である型
  • オブジェクトの動的タイプのcv修飾バージョンに対応する符号付きまたは符号なしタイプのタイプ
  • そのうち、前述のタイプのうちの1つを含む凝集体または共用タイプの要素または非静的データメンバ(を含むが、再帰的に要素又は非静的データメンバ subaggregateまたは含ま連合)、
  • オブジェクトの動的な型の(場合によってはcv修飾された)基本クラス型である型
  • A charまたはunsigned charタイプ。

2つの変更が小さかった:glvalueの代わりに、左辺値、および集計/組合ケースの明確化。

3番目の変更により、より強力な保証が行われます(強力なエイリアシングルールが緩和されます):類似した型の新しいコンセプトがエイリアスに安全になりました。


また、Cの文言(C99; ISO / IEC 9899:1999 6.5 / 7;まったく同じ文言がISO / IEC 9899:2011§6.5¶7でも使用されています):

オブジェクトには、次のタイプ73)または88)のいずれかを持つ左辺値式によってのみアクセスされる格納された値が必要です

  • オブジェクトの有効なタイプと互換性のあるタイプ
  • オブジェクトの有効な型と互換性のある型の修飾バージョン
  • オブジェクトの有効な型に対応する符号付きまたは符号なしの型である型
  • オブジェクトの有効な型の修飾バージョンに対応する符号付きまたは符号なしの型である型
  • メンバーの中に前述のタイプの1つを含む集約タイプまたはユニオンタイプ(再帰的に、サブアグリゲートのメンバーまたは含まれるユニオンを含む)、または
  • 文字タイプ。

73)または88)このリストの目的は、オブジェクトがエイリアスされる場合とされない場合がある状況を指定することです。


7
ベンは、人々がここで指示されることが多いので、完全を期すために、私もC標準への参照を追加できるようにしました。
Kos

1
それについて語っているC89 Rationale cs.technion.ac.il/users/yechiel/CS/C++draft/rationale.pdfセクション3.3を見てください。
phorgan1 2012年

2
構造型の左辺値があり、メンバーのアドレスを受け取り、それをメンバー型へのポインターとして使用する関数に渡す場合、メンバー型のオブジェクトへのアクセス(法的)と見なされますか?または構造タイプのオブジェクト(禁止)?多くのコードは、それが、このような方法でアクセス構造に法的だと仮定し、私は多くの人がそのような行為を禁止するものとして理解されたルールで甲高い音だと思うが、それは正確な規則が何であるかは不明です。さらに、共用体と構造は同じように扱われますが、それぞれの賢明なルールは異なるはずです。
スーパーキャット2015年

2
@supercat:構造のルールの言い方、実際のアクセスは常にプリミティブ型へのアクセスです。次に、プリミティブタイプへの参照を介したアクセスは、タイプが一致するため正当であり、包含構造タイプへの参照を介したアクセスは、特別に許可されているため正当です。
Ben Voigt 2015年

2
@BenVoigt:ユニオン経由でアクセスしない限り、一般的な初期シーケンスは機能しないと思います。参照goo.gl/HGOyoKを gccがやっているかを確認します。メンバー型の左辺値(union-member-access演算子を使用しない)を介してユニオン型の左辺値にアクセスwow(&u->s1,&u->s2)することが合法である場合、ポインターを使用して変更する場合でも合法である必要があり、これによりu、エイリアシングルールは、容易にするために設計されました。
スーパーキャット2015年

81

注意

これは私の「厳格なエイリアシングルールとは何か、なぜ私たちは気にするのか」からの抜粋です。書き上げる。

厳密なエイリアスとは何ですか?

CおよびC ++では、エイリアシングは、格納された値へのアクセスを許可されている式のタイプに関係しています。CとC ++の両方で、標準では、どの型にエイリアスを付けることができる式の型が指定されています。コンパイラとオプティマイザは、エイリアシングルールに厳密に従っていることを前提としているため、厳密なエイリアシングルールと呼ばれます。許可されていないタイプを使用して値にアクセスしようとすると、未定義の動作UB)として分類されます。未定義の動作が発生すると、すべての賭けが無効になり、プログラムの結果は信頼できなくなります。

残念ながら、厳密なエイリアシング違反があると、予想どおりの結果が得られることが多く、新しい最適化を備えたコンパイラの将来のバージョンでは、有効であると考えたコードが壊れる可能性があります。これは望ましくないことであり、厳密なエイリアシングルールとその違反を回避する方法を理解することは価値のある目標です。

なぜ私たちが気にするのかをさらに理解するために、厳密なエイリアシングルールに違反するときに発生する問題について説明します。

予備的な例

いくつかの例を見てみましょう。次に、規格の発言について正確に話し、さらにいくつかの例を調べて、厳密なエイリアシングを回避し、見逃した違反をキャッチする方法を確認します。これは驚くべきことではない例です(実例):

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

我々は持っているのint *メモリを指すが、によって占めint型とこれが有効なエイリアスです。オプティマイザは、ipによる割り当てがxが占める値を更新できると想定する必要があります。

次の例は、未定義の動作につながるエイリアスを示しています(実際の例):

int foo( float *f, int *i ) { 
    *i = 1;               
    *f = 0.f;            

   return *i;
}

int main() {
    int x = 0;

    std::cout << x << "\n";   // Expect 0
    x = foo(reinterpret_cast<float*>(&x), &x);
    std::cout << x << "\n";   // Expect 0?
}

関数fooでは、int *float *を受け取ります。この例では、fooを呼び出し、両方のパラメーターを、この例ではintを含む同じメモリー位置を指すように設定します。reinterpret_castは、テンプレートパラメーターで指定された型があるかのように式を扱うようにコンパイラーに指示していることに注意してください。この場合、式&xfloat *型であるかのように扱うように指示しています。単純に2番目のcoutの結果が0になると予想する場合がありますが、-O2を使用して最適化を有効にすると、gccとclangの両方が次の結果を生成します。

0
1

これは予想されないかもしれませんが、未定義の動作を呼び出したので完全に有効です。フロートは有効にエイリアスできないint型のオブジェクト。したがって、オプティマイザは、iを逆参照するときに格納された定数1を戻り値と見なすことができます。これは、fを介した格納がintオブジェクトに有効に影響しなかったためです。コンパイラエクスプローラーでコードをプラグインすると、これがまさに何が起こっているかを示しています(実例):

foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1  
mov dword ptr [rdi], 0
mov eax, 1                       
ret

Type-Based Alias Analysis(TBAA)を使用するオプティマイザは、1が返されると想定し、定数値をレジスタeaxに直接移動します。このレジスタは、戻り値を保持します。TBAAは、ロードとストアを最適化するために、どのタイプにエイリアスを許可するかに関する言語規則を使用します。この場合、TBAAは、floatがエイリアスおよびintできないことを認識し、iの負荷を最適化します。

今、ルールブックへ

私たちが許可されていることと許可されていないことを規格は正確に何と言っていますか?標準言語は単純ではないので、各項目について、意味を示すコード例を提供しようと思います。

C11標準は何と言っていますか?

C11の標準は、セクションに次のように述べている6.5式パラグラフ7

オブジェクトは、次のタイプのいずれかを持つ左辺値式によってのみアクセスされる格納された値を持つものとします。88) —オブジェクトの有効なタイプと互換性のあるタイプ。

int x = 1;
int *p = &x;   
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int

—オブジェクトの有効なタイプと互換性のあるタイプの修飾バージョン、

int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int

—オブジェクトの有効なタイプに対応する符号付きまたは符号なしタイプのタイプ

int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to 
                     // the effective type of the object

GCC /打ち鳴らすには、拡張子がある割り当てることができますunsigned int型を** int型を、彼らは互換性のある型でなくても。

—オブジェクトの有効な型の修飾バージョンに対応する署名付きまたは署名なしの型である型

int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type 
                     // that corresponds with to a qualified verison of the effective type of the object

—メンバーの中に前述のタイプの1つを含む集約またはユニオンタイプ(再帰的に、サブアグリゲートまたは含まれるユニオンのメンバーを含む)、または

struct foo {
  int x;
};

void foobar( struct foo *fp, int *ip );  // struct foo is an aggregate that includes int among its members so it can
                                         // can alias with *ip

foo f;
foobar( &f, &f.x );

—文字タイプ。

int x = 65;
char *p = (char *)&x;
printf("%c\n", *p );  // *p gives us an lvalue expression of type char which is a character type.
                      // The results are not portable due to endianness issues.

C ++ 17 Draft Standardの発言

セクション[basic.lval]パラグラフ11のC ++ 17ドラフト標準は次のように述べています。

:次のタイプの他の数の挙動が未定義であるのglvalueを通じてオブジェクトの格納された値にアクセスするためのプログラムしようとした場合63 -オブジェクトの動的な型、(11.1)

void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0};        // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n";        // *ip gives us a glvalue expression of type int which matches the dynamic type 
                                  // of the allocated object

(11.2)—オブジェクトの動的タイプのcv修飾バージョン、

int x = 1;
const int *cip = &x;
std::cout << *cip << "\n";  // *cip gives us a glvalue expression of type const int which is a cv-qualified 
                            // version of the dynamic type of x

(11.3)—オブジェクトの動的タイプに類似したタイプ(7.5で定義)

(11.4)—オブジェクトの動的タイプに対応する符号付きまたは符号なしタイプのタイプ

// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
  si = 1;
  ui = 2;

  return si;
}

(11.5)—オブジェクトの動的タイプのcv修飾バージョンに対応する署名付きまたは署名なしのタイプであるタイプ

signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing

(11.6)—要素または非静的データメンバー(再帰的に、サブアグリゲートまたは含まれるユニオンの要素または非静的データメンバーを含む)の中に前述のタイプの1つを含む集約またはユニオンタイプ、

struct foo {
 int x;
};

// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
 fp.x = 1;
 ip = 2;

 return fp.x;
}

foo f; 
foobar( f, f.x ); 

(11.7)—オブジェクトの動的タイプの(おそらくcv修飾された)基本クラスタイプであるタイプ

struct foo { int x ; };

struct bar : public foo {};

int foobar( foo &f, bar &b ) {
  f.x = 1;
  b.x = 2;

  return f.x;
}

(11.8)— char、unsigned char、またはstd :: byteタイプ。

int foo( std::byte &b, uint32_t &ui ) {
  b = static_cast<std::byte>('a');
  ui = 0xFFFFFFFF;                   

  return std::to_integer<int>( b );  // b gives us a glvalue expression of type std::byte which can alias
                                     // an object of type uint32_t
}

上記のリストには、signed charに注意する価値はありません。これは、文字タイプを表すCとの大きな違いです。

タイププニングとは

ここまで来て、疑問に思うかもしれませんが、なぜ別名を付けたいのでしょうか?答えは通常、pun入力することです。多くの場合、使用されるメソッドは厳密なエイリアス規則に違反しています。

型システムを回避して、オブジェクトを別の型として解釈したい場合があります。これはタイプパニングと呼ばれ、メモリのセグメントを別のタイプとして再解釈します。タイプパニングは、オブジェクトの基になる表現にアクセスして、表示、転送、または操作する必要があるタスクに役立ちます。タイプパニングが使用されているのがわかる典型的な領域は、コンパイラ、シリアル化、ネットワークコードなどです。

伝統的に、これはオブジェクトのアドレスを取得し、それを再解釈したい型のポインターにキャストしてから値にアクセスすることによって、つまり別名を付けることによって実現されていました。例えば:

int x =  1 ;

// In C
float *fp = (float*)&x ;  // Not a valid aliasing

// In C++
float *fp = reinterpret_cast<float*>(&x) ;  // Not a valid aliasing

printf( "%f\n", *fp ) ;

これまで見てきたように、これは有効なエイリアスではないため、未定義の動作を呼び出しています。しかし、伝統的にコンパイラーは厳密なエイリアシング規則を利用せず、このタイプのコードは通常は機能するだけでしたが、開発者は残念ながらこの方法で慣れてきました。タイプパニングの一般的な代替方法は、Cで有効であるが、C ++では未定義の動作である共用体を使用する方法です(実際の例を参照)。

union u1
{
  int n;
  float f;
} ;

union u1 u;
u.f = 1.0f;

printf( "%d\n”, u.n );  // UB in C++ n is not the active member

これはC ++では無効であり、一部のユーザーは、共用体の目的はバリアント型を実装するためだけであると考えており、型のパンニングに共用体を使用することは不正行為だと感じています。

Punを正しく入力するにはどうすればよいですか?

CとC ++の両方で型パンニングを行うための標準的な方法はmemcpyです。これは少し重いように見えるかもしれませんが、オプティマイザは型パンニングのためのmemcpyの使用を認識し、それを最適化して、レジスタの移動を生成する必要があります。たとえば、int64_tdoubleと同じサイズであることがわかっている場合:

static_assert( sizeof( double ) == sizeof( int64_t ) );  // C++17 does not require a message

memcpyを使用できます。

void func1( double d ) {
  std::int64_t n;
  std::memcpy(&n, &d, sizeof d); 
  //...

十分な最適化レベルでは、適切な最新のコンパイラは、前述のreinterpret_castメソッドまたは型パンニングのunionメソッドと同じコードを生成します。生成されたコードを調べると、register mov(ライブコンパイラエクスプローラーの例)のみを使用していることがわかります。

C ++ 20とbit_cast

C ++ 20では、bit_cast提案からのリンクで利用可能な実装)を得ることができます。これは、タイプパンするためのシンプルで安全な方法を提供するだけでなく、constexprコンテキストで使用可能です。

以下は、bit_castを使用して、unsigned intfloatに入力する方法の例です(実際に表示されます)。

std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)

場合より型が同じサイズを持っていない、それは中間struct15を使用するために私たちを必要とします。我々は含む構造体に使用するのsizeof(unsigned int型)文字配列を(想定している4バイトの符号なし整数をする)から、タイプとunsigned int型としては、するにはタイプ:

struct uint_chars {
 unsigned char arr[sizeof( unsigned int )] = {} ;  // Assume sizeof( unsigned int ) == 4
};

// Assume len is a multiple of 4 
int bar( unsigned char *p, size_t len ) {
 int result = 0;

 for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
   uint_chars f;
   std::memcpy( f.arr, &p[index], sizeof(unsigned int));
   unsigned int result = bit_cast<unsigned int>(f);

   result += foo( result );
 }

 return result ;
}

この中間型が必要なのは残念ですが、それが現在のbit_castの制約です。

厳格なエイリアシング違反の検出

C ++で厳密なエイリアシングをキャッチするための優れたツールは多くありません。厳密なエイリアシング違反のいくつかのケースや、整列されていないロードとストアのいくつかのケースをキャッチするツールがあります。

フラグ-fstrict -aliasing-Wstrict-aliasingを使用するgccは、誤検知 /誤検知がないわけではありませんが、いくつかのケースをキャッチできます。たとえば、次の場合はgccで警告が生成されます(ライブで確認してください)。

int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught 
               // it was being accessed w/ an indeterminate value below

printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));

この追加のケースをキャッチしません(ライブでご覧ください):

int *p;

p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));

clangはこれらのフラグを許可しますが、実際には警告を実装していないようです。

私たちが利用できるもう1つのツールはASanで、これは整列されていないロードとストアをキャッチできます。これらは直接の厳密なエイリアシング違反ではありませんが、厳密なエイリアシング違反の一般的な結果です。たとえば、次の場合、-fsanitize = addressを使用してclangでビルドするとランタイムエラーが発生します

int *x = new int[2];               // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6);     // regardless of alignment of x this will not be an aligned address
*u = 1;                            // Access to range [6-9]
printf( "%d\n", *u );              // Access to range [6-9]

私がお勧めする最後のツールはC ++固有のものであり、厳密にはツールではありませんが、コーディングの実践であり、Cスタイルのキャストは許可されていません。gccとclangはどちらも、-Wold-style-castを使用してCスタイルのキャストの診断を生成します。これにより、未定義の型パンがreinterpret_castを使用するように強制されます。一般に、reinterpret_castは、より詳細なコードレビューのためのフラグである必要があります。また、コードベースでreinterpret_castを検索して監査を実行する方が簡単です。

Cの場合は、すべてのツールがすでにカバーされています。また、C言語の大部分のプログラムを徹底的に分析する静的アナライザーであるtis-interpreterもあります。-fstrict -aliasingを使用すると1つのケースが見落とされる、前の例のCバージョンが指定されている場合(ライブで表示

int a = 1;
short j;
float f = 1.0 ;

printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));

int *p; 

p=&a;
printf("%i\n", j = *((short*)p));

tis-interpeterは3つすべてをキャッチできます。次の例では、tis-kernalをtis-interpreterとして呼び出します(出力は簡潔にするために編集されています)。

./bin/tis-kernel -sa example1.c 
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
              rules by accessing a cell with effective type int.
...

example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
              accessing a cell with effective type float.
              Callstack: main
...

example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
              accessing a cell with effective type int.

最後に、現在開発中のTySanがあります。このサニタイザーは、シャドウメモリセグメントに型チェック情報を追加し、アクセスをチェックして、エイリアスルールに違反していないかどうかを確認します。このツールは、すべてのエイリアス違反をキャッチできる可能性がありますが、実行時のオーバーヘッドが大きくなる可能性があります。


コメントは詳細な議論のためのものではありません。この会話はチャットに移動しました
Bhargav Rao

3
できれば、+ 10、よく書かれ、説明されています。また、コンパイラの作成者とプログラマの両方から...唯一の批判:上記の反例を持っていると、標準で禁止されていることを確認できるので、明白ではありません。種類:-)
ガブリエル

2
とても良い答えです。最初の例がC ++で提供されていることを後悔しているだけです。そのため、Cだけを知っているか気にしていて、何reinterpret_castができるか、何をcout意味するのかわからない私のような人々にとって、従うのは困難です。(C ++について言及しても問題ありませんが、元の質問はCおよびIIUCに関するものでした。これらの例は、Cでも同様に正しく記述できます。)
Gro-Tsen

タイプパニングに関して:したがって、あるタイプXの配列をfileに書き込み、そのファイルからこの配列をvoid *でポイントされたメモリに読み取った場合、それを使用するためにそのポインターをデータの実際のタイプにキャストします-それは未定義の動作?
マイケルIV 4

44

厳密なエイリアシングはポインターだけを参照するのではなく、参照にも影響します。ブースト開発者wikiについてそれについて論文を書きましたが、非常に評判が良かったので、コンサルティングWebサイトのページに変えました。それはそれが何であるか、それが人々をそれほど混乱させる理由とそれについて何をすべきかを完全に説明しています。厳密なエイリアシングホワイトペーパー。特に、ユニオンがC ++にとって危険な動作である理由、およびmemcpyの使用がCとC ++の両方で移植可能な唯一の修正である理由を説明しています。これがお役に立てば幸いです。


3
厳密なエイリアシングはポインターだけを参照するのではなく、参照にも影響を与えます」実際には、左辺値を参照します。" memcpyを使用することが唯一の修正ポータブルです "聞いてください!
curiousguy

5
良い紙。私の見解:(1)このエイリアシング-「問題」は、悪いプログラミングに対する過剰な反応です-悪いプログラマを自分の悪い習慣から保護しようとします。プログラマーが良い習慣を持っている場合、このエイリアシングは迷惑であり、チェックを安全にオフにすることができます。(2)コンパイラー側の最適化は、よく知られたケースでのみ行うべきであり、疑わしい場合は、ソースコードを厳密に守る必要があります。プログラマーにコンパイラーの特異性に対応するコードを書かせることは、簡単に言えば間違っています。それを標準の一部にすることはさらに悪いことです。
slashmais 2015

4
@slashmais(1)「不適切なプログラミングに対する過剰反応です」ナンセンス。それは悪い習慣の拒絶です。あなたはそれをしますか?あなたは価格を支払います:あなたのための保証はありません!(2)よく知られているケース?どれ?厳密なエイリアシングルールは「よく知られている」はずです。
curiousguy

5
@curiousguy:混乱のいくつかの点を片付けたので、エイリアシングルールを備えたC言語によって、プログラムが型にとらわれないメモリプールを実装することが不可能になることは明らかです。一部の種類のプログラムはmalloc / freeで問題なく動作しますが、他の種類のプログラムでは、現在のタスクに合わせて調整されたメモリ管理ロジックが必要です。C89の理論的根拠がエイリアシング規則の理由のこのようなくだらない例を使用したのはなぜかと思います。それらの例は、規則が合理的なタスクの実行に大きな困難をもたらさないように見えるからです。
スーパーキャット2015年

5
@curiousguy、ほとんどのコンパイラスイートには-O3のデフォルトとして-fstrict-aliasingが含まれており、この隠された契約はTBAAについて聞いたことがないユーザーに強制され、システムプログラマーのようにコードを記述しました。私はシステムプログラマーに不誠実に聞こえるつもりはありませんが、この種の最適化は-O3のデフォルトのoptの外に残されるべきであり、TBAAが何であるかを知っている人のためのオプトイン最適化であるべきです。特にユーザーコードのソースレベル違反を追跡するTBAAに違反するユーザーコードであることが判明したコンパイラの「バグ」を見るのは楽しいことではありません。
kchoi

34

Doug T.がすでに書いたものへの補足として、これはおそらくgccでそれをトリガーする簡単なテストケースです:

check.c

#include <stdio.h>

void check(short *h,long *k)
{
    *h=5;
    *k=6;
    if (*h == 5)
        printf("strict aliasing problem\n");
}

int main(void)
{
    long      k[1];
    check((short *)k,k);
    return 0;
}

でコンパイルしgcc -O2 -o check check.cます。通常(私が試したほとんどのgccバージョンでは)、これは「厳密なエイリアシングの問題」を出力します。これは、コンパイラが「チェック」関数の「h」を「k」と同じアドレスにすることはできないためです。そのため、コンパイラーはif (*h == 5)アウェイを最適化し、常にprintfを呼び出します。

ここで興味のある人のために、x64用のubuntu 12.04.2で実行されているgcc 4.6.3によって生成されたx64アセンブラコードがあります:

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

したがって、if条件はアセンブラーコードから完全になくなります。


2番目のshort * jをcheck()に追加して使用すると(* j = 7)、hとjが実際に同じ値を指していない場合、ggcはそうしないため、最適化は消えます。はい、最適化は本当に賢いです。
フィリップラディ

2
物事をより楽しいものにするために、互換性はないがサイズと表現が同じである型へのポインターを使用します(eg long long*int64_t*に当てはまる一部のシステムでは)。正常なコンパイラはa long long*を認識し、int64_t*それらが同じように格納されている場合は同じストレージにアクセスできると期待するかもしれませんが、そのような扱いはもはや流行ではありません。
スーパーキャット2017年

Grr ... x64はMicrosoftの規則です。代わりにamd64またはx86_64を使用してください。
SSアン

Grr ... x64はMicrosoftの規則です。代わりにamd64またはx86_64を使用してください。
SSアン

17

(ユニオンを使用するのではなく)ポインターキャストによる型パンニングは、厳密なエイリアシングを解除する主な例です。


1
関連する引用、特に脚注については、ここで私の回答を参照してください。ただし、最初は不適切な表現でしたが、Cでは、和集合による型抜きが常に許可されていました。あなたは私の答えを明確にしたいと思います。
Shafik Yaghmour 2014

@ShafikYaghmour:C89は明らかに、実装者がユニオンを介して型パンニングを認識するか、または認識しないケースを選択することを許可しました。たとえば、プログラマーが書き込みと読み取りの間に次のいずれかを行った場合、実装では、あるタイプへの書き込みの後に別のタイプの読み取りがタイプパニングとして認識されるように指定できます。(1)を含む左辺値を評価するユニオンタイプ[メンバーのアドレスを取得すると、シーケンスの適切なポイントで実行された場合に修飾されます]; (2)ある型へのポインターを別の型へのポインターに変換し、そのptrを介してアクセスします。
スーパーキャット2017年

@ShafikYaghmour:たとえば、整数値と浮動小数点値の間の型パンニングがコードがfpsync()fpとしての書き込みとintとしての読み取りまたはその逆のディレクティブを実行した場合にのみ確実に機能するように実装を指定することもできます(個別の整数およびFPUパイプラインとキャッシュを使用する実装で) 、そのようなディレクティブは高価になる可能性がありますが、コンパイラーがすべてのユニオンアクセスでそのような同期を実行するほど高価ではありません]。または、実装は、共通の初期シーケンスを使用する状況を除いて、結果の値が使用できないことを指定できます。
スーパーキャット2017年

@ShafikYaghmour:C89のもとでは、実装ユニオン経由を含むほとんどの形式の型パンニングを禁止できましたが、ユニオンへのポインターとそのメンバーへのポインターの同等性は、明示的に禁止していない実装でタイプパンニングが許可されていることを意味していました。
スーパーキャット2017年

17

C89の理論的根拠によれば、標準の作成者は、コンパイラーに次のようなコードが与えられることを要求したくありませんでした:

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

を指すx可能性を可能にするために、割り当てとreturnステートメントの間の値を再ロードする必要があり、結果としてへの割り当てがの値を変更する可能性があります。コンパイラーが、上記のような状況ではエイリアシングが発生しないことを前提とする権利を与えられるべきであるという考えは、議論の余地がないものでした。px*px

残念ながら、C89の作成者は、文字どおりに読んだ場合、次の関数でさえ未定義の動作を呼び出すようにルールを記述しました。

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

それは種類の左辺値を使用しているためint、アクセスするタイプのオブジェクトをstruct S、およびintアクセスに使用することができる種類の中ではありませんstruct S。構造体と共用体の非文字型メンバーのすべての使用を未定義の動作として扱うのはばかげているので、ほとんどの人は、ある型の左辺値が別の型のオブジェクトにアクセスするために使用される可能性がある少なくともいくつかの状況があることを認識しています。残念ながら、C標準委員会はそれらの状況が何であるかを定義することに失敗しました。

問題の多くは、次のようなプログラムの動作について尋ねた不具合レポート#028の結果です。

int test(int *ip, double *dp)
{
  *ip = 1;
  *dp = 1.23;
  return *ip;
}
int test2(void)
{
  union U { int i; double d; } u;
  return test(&u.i, &u.d);
}

欠陥レポート#28は、タイプ「double」の共用体メンバーを書き込み、タイプ「int」の1つを読み取るアクションが実装定義の動作を呼び出すため、プログラムが未定義の動作を呼び出すと述べています。そのような推論は無意味ですが、元の問題に対処するために何もせずに言語を不必要に複雑にする効果的な型ルールの基礎を形成します。

元の問題を解決する最良の方法は、ルールの目的に関する脚注を規範的であるかのように扱い、実際にエイリアスを使用したアクセスの競合が関係する場合を除いて、ルールを強制不可能にすることでしょう。次のようなものが与えられた:

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   s.x = 1;
   p = &s.x;
   inc_int(p);
   return s.x;
 }

inc_int介し*pてアクセスされるストレージへのすべてのアクセスはtypeの左辺値で行われるため、内部での競合はありません。またinttestからp目に見えるように派生struct Sし、次回s使用されるまで、そのストレージへのすべてのアクセスが行われるため、競合は発生しません。pがすでに起こっているでしょう。

コードが少し変更された場合...

 void inc_int(int *p) { *p = 3; }
 int test(void)
 {
   int *p;
   struct S { int x; } s;
   p = &s.x;
   s.x = 1;  //  !!*!!
   *p += 1;
   return s.x;
 }

ここで、ps.xマークされた行へのアクセスの間にエイリアスの競合があります。実行のその時点で、同じストレージへのアクセスに使用される別の参照が存在するためです

欠陥レポート028によると、2つのポインターの作成と使用が重複しているため、元の例ではUBが呼び出されたため、「Effective Types」やその他の複雑な要素を追加しなくても、より明確になります。


結局のところ、それほど複雑にせずに目標を達成した、多かれ少なかれ「標準委員会ができること」であった種類の提案を読むのは興味深いでしょう。
jrh 2018

1
@jrh:かなり簡単だと思います。1.関数またはループの特定の実行中にエイリアシングが発生するためには、その実行中に2つの異なるポインターまたは左辺値を使用して、競合するモードの同じストレージをアドレス指定する必要があります。2. 1つのポインターまたは左辺値が別のポインターから新しく目に見える形で導出されるコンテキストでは、2番目のポインターへのアクセスは最初のポインターへのアクセスであることを認識してください。3.この規則は、実際にエイリアシングを伴わない場合に適用することを意図していないことを認識してください。
スーパーキャット2018

1
コンパイラーが新たに導出された左辺値を認識する正確な状況は、実装の品質の問題である可能性がありますが、リモートから適切なコンパイラーは、gccとclangが意図的に無視する形式を認識できるはずです。
スーパーキャット2018

11

答えの多くを読んだ後、何かを追加する必要があると感じます。

厳密なエイリアス(少し説明します)は重要です

  1. メモリアクセスはコストがかかる(パフォーマンスに関して)可能性があるため、物理メモリに書き戻す前にCPUレジスタでデータを操作します。

  2. 2つの異なるCPUレジスタのデータが同じメモリ空間に書き込まれる場合、Cでコーディングするときにどのデータが「存続」するかを予測できません

    CPUレジスタのロードとアンロードを手動でコーディングするアセンブリでは、どのデータがそのまま残っているかがわかります。しかし、Cは(ありがたいことに)この詳細を抽象化します。

2つのポインタがメモリ内の同じ場所を指すことができるため、これにより、衝突の可能性を処理する複雑なコードが生成される可能性があります。

この余分なコードは遅く、(おそらく)不要な余分なメモリの読み取り/書き込み操作を実行するため、パフォーマンスが低下します。

厳格なエイリアシング規則は、私たちは冗長なマシンコードを避けることができ、いる場合でなければなりません(も参照の二つのポインタが同じメモリブロックを指していないと仮定しても安全restrictキーワードを)。

Strictエイリアシングは、異なる型へのポインタがメモリ内の異なる場所を指していると想定することが安全であると述べています。

コンパイラが2つのポインタが異なる型(たとえば、int *とa float *)を指していることに気づいた場合、メモリアドレスが異なると見なされ、メモリアドレスの衝突から保護されないため、マシンコードが高速になります。

次の関数を想定します。

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

a == b(両方のポインターが同じメモリを指す)場合に対処するために、メモリからCPUレジスタにデータをロードする方法を順序付けてテストする必要があるため、コードは次のようになります。

  1. ロードaおよびbメモリから。

  2. に追加abます。

  3. 保存 bして再読み込みし aます。

    (CPUレジスタからメモリに保存し、メモリからCPUレジスタにロードします)。

  4. に追加baます。

  5. a(CPUレジスタから)メモリに保存します。

手順3は物理メモリにアクセスする必要があるため、非常に低速です。ただし、同じメモリアドレスab指すインスタンスから保護する必要があります。

厳密なエイリアシングにより、これらのメモリアドレスが明確に異なることをコンパイラに通知することで、これを防ぐことができます(この場合、ポインタがメモリアドレスを共有している場合は実行できないさらなる最適化が可能になります)。

  1. これは、異なるタイプを使用してポイントすることにより、2つの方法でコンパイラーに伝えることができます。つまり:

    void merge_two_numbers(int *a, long *b) {...}
  2. restrictキーワードを使用します。すなわち:

    void merge_two_ints(int * restrict a, int * restrict b) {...}

ここで、Strict Aliasingルールを満たすことにより、ステップ3を回避でき、コードの実行速度が大幅に向上します。

実際、restrictキーワードを追加することで、関数全体を次のように最適化できます。

  1. ロードaおよびbメモリから。

  2. に追加abます。

  3. aとの両方に結果を保存しbます。

この最適化可能性があるため、衝突(ここで、以前に行われていることができませんでしたaし、b三倍の代わりに倍増することでしょう)。


ステップ3でキーワードを制限して、結果を「b」だけに保存​​しないでください。合計の結果も「a」に格納されるように聞こえます。「b」をもう一度リロードする必要がありますか?
NeilB 2018年

1
@NeilB-そうだね。保存b(再読み込みではなく)と再読み込みのみを行いますa。私はそれが今より明確であることを望みます。
Myst

タイプベースのエイリアシングは以前にいくつかの利点を提供していたかもしれませんrestrictが、後者の方がほとんどの状況でより効果的であり、いくつかの制約を緩めるregisterと、restrict役に立たない場合のいくつかを埋めることができると思います。標準を特定の証拠が存在しない場合でもコンパイラがエイリアスを推定する必要がある場所を単に説明するのではなく、コンパイラがエイリアスの証拠を認識することを期待するすべてのケースを完全に説明するように標準を扱うことが「重要」であったかどうかはわかりません。
スーパーキャット2018年

メインRAMからのロードは非常に遅い(そして、次の操作が結果に依存する場合、CPUコアを長時間停止させる可能性があります)が、L1キャッシュからのロードはかなり高速であり、最近書き込んでいたキャッシュラインに書き込むことに注意してください同じコアによって。したがって、アドレスへの最初の読み取りまたは書き込みを除くすべてが通常はかなり高速になります。reg/ mem addrアクセス​​の違いは、キャッシュされた/キャッシュされていないmem addrの違いよりも小さくなります。
curiousguy

@curiousguy-あなたは正しいですが、この場合の「速い」は相対的です。L1キャッシュは、おそらくCPUレジスタよりも1桁遅い(おそらく10倍以上遅いと思います)。さらに、このrestrictキーワードは操作の速度だけでなくその数も最小化します。これは意味があるかもしれません...つまり、結局のところ、最速の操作はまったく操作がないことです:)
Myst

6

厳密なエイリアシングでは、同じデータへの異なるポインタ型は許可されていません。

この記事は、問題を完全に理解するのに役立ちます。


4
参照間、および参照とポインタ間でエイリアスを作成することもできます。私のチュートリアルを参照dbp-consulting.com/tutorials/StrictAliasing.html
phorgan1

4
同じデータへの異なるポインタ型を使用することが許可されています。厳密なエイリアシングが発生するのは、同じメモリ位置が1つのポインタタイプを介して書き込まれ、別のポインタタイプを介して読み取られる場合です。また、いくつかの異なるタイプが許可されています(例:intを含む構造体int)。
MM

-3

技術的にはC ++では、厳密なエイリアシングルールはおそらく適用されません。

間接指定(*演算子)の定義に注意してください。

単項*演算子は間接参照を実行します。適用される式は、オブジェクト型へのポインター、または関数型へのポインターであり、結果は、が指すオブジェクトまたは関数参照する左辺値です。

また、glvalueの定義から

glvalueは、その評価がオブジェクトのアイデンティティを決定する式です(... snip)

したがって、明確に定義されたプログラムトレースでは、glvalueはオブジェクトを参照します。したがって、いわゆる厳密なエイリアシングルールは適用されません。これは、設計者が望んでいたものとは異なる場合があります。


4
C標準では、「オブジェクト」という用語を使用して、さまざまな概念を示しています。これらの中で、排他的に、いくつかの目的に割り当てられるバイトのシーケンス、特定の型の値がそこから/へのバイトの配列に未必ずしも排他参照することができる読み書き、またはそのような参照実際に有していますあるコンテキストでアクセスされた、またはアクセスされる。「オブジェクト」という用語を定義する賢明な方法は、標準がそれを使用するすべての方法と一致するとは思いません。
スーパーキャット2018

@supercat不正解です。あなたの想像力にもかかわらず、それは実際にはかなり一貫しています。ISO Cでは、「内容が値を表すことができる実行環境内のデータストレージの領域」として定義されています。ISO C ++にも同様の定義があります。あなたのコメントは回答よりもさらに重要ではありません。なぜなら、あなたが言及したのは、オブジェクトのコンテンツを参照する表現の方法であり、回答は、オブジェクトのアイデンティティに密接に関連する一種の式のC ++概念(glvalue)を示しているからです。そして、すべてのエイリアシングルールは基本的にアイデンティティに関連しますが、コンテンツには関連しません。
FrankHB

1
@FrankHB:宣言した場合int foo;、左辺値式は何にアクセスし*(char*)&fooますか?それはタイプのオブジェクトcharですか?そのオブジェクトは同時に存在しfooますか?foo前述のタイプのオブジェクトの格納された値を変更するために書き込みcharますか?もしそうならchar、タイプの左辺値を使用してタイプのオブジェクトの格納された値にアクセスできるようにするルールはありますintか?
スーパーキャット

@FrankHB:6.5p7がない場合、ストレージのすべての領域には、そのストレージの領域に収まるすべてのタイプのすべてのオブジェクトが同時に含まれ、そのストレージの領域にアクセスすると、それらのすべてに同時にアクセスすると簡単に言えます。ただし、6.5p7での「オブジェクト」という用語の使用をこのような方法で解釈すると、文字型以外の左辺値を使用してほとんどのことを実行できなくなり、明らかに不合理な結果となり、規則の目的が完全に無効になります。さらに、6.5p6以外で使用されている「オブジェクト」の概念には、静的なコンパイル時の型がありますが、...
スーパーキャット

1
sizeof(int)は4で、宣言int i;は各文字型in addition to one of type int ? I see no way to apply a consistent definition of "object" which would allow for operations on both *(char *)&i`およびの4つのオブジェクトを作成しiますか?最後に、volatile「オブジェクト」の定義を満たさないハードウェアレジスタにアクセスするための-qualifiedポインタを許可する標準はありません。
スーパーキャット
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.