web-dev-qa-db-ja.com

ポインタ演算または比較の制限の理由は何ですか?

C/C++では、 ポインタの加算または減算 は、結果のポインタが元のポイントされた 完全なオブジェクト 内にある場合にのみ定義されます。さらに、2つのポインタの 比較 は、2つのポイントされたオブジェクトが一意の完全なオブジェクトのサブオブジェクトである場合にのみ実行できます。

そのような制限の理由は何ですか?

セグメント化されたメモリモデル( ここ §1.2.1を参照)が理由の1つである可能性があると思いましたが、コンパイラは実際には、これが示すようにすべてのポインタの全順序を定義できるため answer =、私はこれを疑っています。

26
Oliv

プログラムとデータスペースが分離されているアーキテクチャがあり、2つの任意のポインタを減算するのは単純に不可能です。関数またはconst静的データへのポインターは、通常の変数とは完全に異なるアドレス空間にあります。

異なるアドレス空間間で任意にランキングを指定した場合でも、diff_tタイプはより大きなサイズである必要があります。また、2つのポインターを比較または減算するプロセスは、非常に複雑になります。これは、速度を重視して設計された言語では悪い考えです。

10
Mark Ransom

その理由は、妥当なコードを生成する可能性を維持するためです。これは、フラットメモリモデルを備えたシステムだけでなく、より複雑なメモリモデルを備えたシステムにも当てはまります。配列の加算や減算、オブジェクト間のポインターの全順序の要求など、(あまり役に立たない)コーナーケースを禁止する場合は、生成されたコードの多くのオーバーヘッドをスキップできます。

標準によって課せられた制限により、コンパイラーはポインター演算について仮定を立て、これを使用してコードの品質を向上させることができます。実行時ではなくコンパイラで静的に計算することと、使用する命令とアドレッシングモードを選択することの両方をカバーしています。例として、2つのポインターp1p2を持つプログラムを考えてみましょう。コンパイラが異なるデータオブジェクトを指していることを導き出すことができれば、次のp1に基づく操作が、p2が指すオブジェクトに影響を与えることはないと安全に想定できます。これにより、コンパイラはp1に基づくロードとストアを考慮せずに、p2に基づいてロードとストアを並べ替えることができます。

11
Johan

制限を取り除くことができることを証明するだけですが、Cの目標に反するコスト(メモリとコードの観点から)が伴うことを見逃します。

具体的には、違いはptrdiff_tであるタイプを持つ必要があり、size_tに類似していると想定されます。

セグメント化されたメモリモデルでは、(通常)間接的にオブジェクトのサイズに制限があります-答えが次のようになっていると仮定します: `size_t`、` uintptr_t`、 `intptr_t`、および` ptrdiff_t`タイプの実際のサイズは何ですかセグメント化されたアドレス指定モードを使用する16ビットシステムでは? は正しいです。

したがって、少なくとも違いについては、その制限を削除すると、(他の回答のように)重要でないコーナーケースの場合、全体の順序を保証するための追加の指示が追加されるだけでなく、違いなどのために2倍のメモリ量が費やされます。

Cは、よりミニマルであり、コンパイラがそのような場合にメモリとコードを消費することを強制しないように設計されました。 (当時、メモリの制限はもっと重要でした。)

明らかに、他の利点もあります-異なる配列からのポインターを混合するときにエラーを検出する可能性など。同様に、2つの異なるコンテナーのイテレーターの混合はC++では定義されていません(いくつかのマイナーな例外を除く)-そしていくつかのデバッグ実装はそのようなエラーを検出します。

10
Hans Olsson

理論的根拠は、一部のアーキテクチャではメモリがセグメント化されており、異なるオブジェクトへのポインタが異なるメモリセグメントを指している可能性があるためです。その場合、2つのポインターの違いは、必ずしも意味のあるものではありません。

これは、以前の標準Cにまでさかのぼります。Cの理論的根拠はこれを明示的に言及していませんが、負の配列インデックスの使用が未定義の動作である理由を説明する場所を見ると、これが理由であることを示唆しています(C99理論的根拠5.106.5.6、強調鉱山):

一方、p-1の場合、pがトラバースするオブジェクトの配列の前にオブジェクト全体を割り当てる必要があるため、配列の下部から実行されるデクリメントループが失敗する可能性があります。 この制限により、たとえば、セグメント化されたアーキテクチャでは、アドレス可能なメモリの範囲の先頭にオブジェクトを配置できます。

3
Lundin

Stanadrdがアクションを未定義の振る舞いを呼び出すものとして分類するほとんどの場合、次の理由で分類されています。

  1. 動作の定義に費用がかかるプラットフォームがあるかもしれません。コードがオブジェクトの境界を超えて拡張するポインタ演算を実行しようとすると、セグメント化されたアーキテクチャが奇妙に動作する可能性があり、一部のコンパイラはp > qの符号をテストしてq-pを評価する場合があります。

  2. 動作の定義が役に立たないプログラミングにはいくつかの種類があります。多くの種類のコードは、標準で指定されているものを超えるポインターの加算、減算、または関係比較の形式に依存することなく、問題なく実行できます。

  3. さまざまな目的でコンパイラを作成する人々は、そのような目的で意図された高品質のコンパイラが予測どおりに動作する必要がある場合を認識し、標準で強制されているかどうかに関係なく、適切な場合にそのような場合を処理できる必要があります。

#1と#2はどちらも非常に低いバーであり、#3は「ギミー」であると考えられていました。低水準プログラミングを目的とした高品質の実装によって動作が定義されたコードを壊す方法を見つけることによってコンパイラー作成者が巧妙さを誇示するのは流行になっていますが、標準の作成者はコンパイラー作成者が巨大なものを認識することを期待していなかったと思います予測どおりに動作する必要のあるアクションと、ほぼすべての高品質の実装が同じように動作することが期待されるアクションとの違いですが、一部の難解な実装に何か他のことをさせることが役立つと考えられます。

2
supercat

C標準はプロセッサアーキテクチャの大部分をカバーすることを意図しているので、これもカバーする必要があります。ポインタが単なる数字ではなく、構造または「記述子」のようなアーキテクチャを想像してみてください(私は知っていますが、名前は付けません)。 "。このような構造には、それが指すオブジェクト(仮想アドレスとサイズ)およびその中のオフセットに関する情報が含まれています。ポインタを加算または減算すると、オフセットフィールドのみが調整された新しい構造が生成されます。オブジェクトのサイズよりも大きいオフセットを持つ構造を作成することは、ハードウェアで禁止されています。他の制限(初期記述子の生成方法やそれを変更する他の方法など)がありますが、それらはトピックに関連していません。

2

質問を逆にして答えたいと思います。ポインターの加算とほとんどの算術演算が許可されない理由を尋ねる代わりに、ポインターが整数の加算または減算、ポストとプリのインクリメント、および同じ配列を指すポインターのデクリメントと比較(または減算)のみを許可するのはなぜですか?これは、算術演算の論理的帰結と関係があります。整数nをポインタpに加算/減算すると、現在ポイントされている要素から順方向または逆方向のn番目の要素のアドレスが得られます。同様に、同じ配列を指すp1とp2を引くと、2つのポインター間の要素の数がわかります。ポインターの算術演算が、それが指している変数のタイプと一致して定義されているという事実(または設計)は、天才の本当のストロークです。許可された操作以外の操作は、プログラミングまたは哲学的に論理的な推論に反するため、許可されません。

1
Seshadri R

(賛成しないでください)これは答えの要約であり、私がMark Ransomの答えを選んだ理由、そしてこの投稿をするために:答えはその投稿とそれに続くコメントです。

質問はそのような制限の理由は何ですか(ポインタの演算と比較は一意の完全なオブジェクトに当てはまる必要があります)?

身代金のコメント+回答の要約:アドレス空間の概念がないため、ポインター演算をアドレス空間内に制限する唯一の可能性は、オブジェクトに制約することでした。

Krazy Glewのコメントは、社会学的な答えも提供します。

0
Oliv