サイズ0の動的配列へのポインタの増分は未定義ですか?


34

AFAIK、ただし、サイズが0の静的メモリ配列を作成することはできませんが、動的配列を使用して作成できます。

int a[0]{}; // Compile-time error
int* p = new int[0]; // Is well-defined

私が読んだpように、1つの最後の要素のように動作します。p指すアドレスを印刷できます。

if(p)
    cout << p << endl;
  • イテレータ(最後の要素)ではできないので、そのポインタ(最後の要素)を逆参照できないことは確かですが、そのポインタをインクリメントするかどうかはわかりませんp。イテレータのような未定義の動作(UB)ですか?

    p++; // UB?

4
UB "...他の状況(つまり、同じ配列の要素または最後の1つを超えてポイントしていないポインターを生成しようとする試み)は、未定義の動作を呼び出します..." from:en.cppreference.com / w / cpp / language / operator_arithmetic
Richard Critten

3
さて、これはそれにstd::vector0アイテムを含むに似ています。begin()はすでに等しいend()ので、最初を指しているイテレータをインクリメントすることはできません。
Phil1970

1
@PeterMortensenあなたの編集により最後の文の意味が変わったと思います(「私が確信していること->理由がわからない」)、もう一度確認してください。
ファビオはモニカを復活させます

@PeterMortensen:編集した最後の段落が少し読みにくくなりました。
イタチうちわ

回答:


32

配列の要素へのポインタは、有効な要素、または最後を過ぎた要素を指すことができます。ポインタが最後を超えていくようにインクリメントした場合の動作は未定義です。

サイズが0の配列の場合、pはすでに1端を超えているため、増分することはできません。

+演算子についてはC ++ 17 8.7 / 4を参照してください(++同じ制限があります):

式f P要素を指しx[i]配列オブジェクトのx有するn個の要素の表現P + JJ + PJ値が持っているj(おそらく、仮想的な)要素へ)ポイントx[i+j]0≤i+j≤n場合。それ以外の場合、動作は未定義です。


2
これだけの場合は、x[i]同じであるx[i + j]ときもあるiし、j値0を持っていますか?
Rami Yen、

8
@RamiYen x[i]は、if と同じ要素x[i+j]ですj==0
interjay

1
うーん、C ++のセマンティクスの「薄明ゾーン」は嫌いです... +1でも。
einpoklum

4
@ einpoklum-reinstateMonica:実際にはトワイライトゾーンはありません。N = 0の場合でもC ++が一貫しているだけです。N要素の配列の場合、配列の後ろを指すことができるため、N + 1の有効なポインター値があります。つまり、配列の先頭から開始し、ポインタをN回インクリメントして最後に到達できます。
MSalters

1
@MaximEgorushkin私の答えは、言語が現在許可しているものについてです。代わりに許可したいという議論はトピック外です。
インタージェイ

2

私はあなたがすでに答えを持っていると思います。少し深く見てみると、オフザエンドのイテレータをインクリメントすることはUBであると言っていました。つまり、この答えはイテレータとは何ですか?

イテレータはポインタを持つオブジェクトであり、イテレータが持っているポインタを実際にインクリメントしていることを示します。したがって、多くの側面で、イテレーターはポインターの観点から処理されます。

int arr [] = {0,1,2,3,4,5,6,7,8,9};

int * p = arr; // pはarrの最初の要素を指します

++ p; // pはarr [1]を指します

イテレータを使用してベクトルの要素をトラバースできるのと同じように、ポインタを使用して配列の要素をトラバースできます。もちろん、そのためには、最初の要素と最後の要素の1つ前の要素へのポインタを取得する必要があります。今見たように、配列自体を使用するか、最初の要素のアドレスを取得することで、最初の要素へのポインタを取得できます。配列の別の特別なプロパティを使用して、オフザエンドのポインターを取得できます。存在しない要素のアドレスを、配列の最後の要素の1つ前に取得できます。

int * e =&arr [10]; // arrの最後の要素の直後のポインタ

ここでは、添え字演算子を使用して、存在しない要素にインデックスを付けました。arrには10個の要素があるため、arrの最後の要素はインデックス位置9にあります。この要素で実行できる唯一のことは、eを初期化するために行うアドレスの取得です。オフザエンドのイテレータ(§3.4.1、p。106)と同様に、オフザエンドのポインタは要素を指しません。その結果、オフエンドのポインターを逆参照またはインクリメントすることはできません。

これは、LipmannによるC ++プライマー5エディションからのものです。

ですからUBはやらないでください。


-4

厳密には、これは未定義の動作ではなく、実装によって定義されます。したがって、非主流のアーキテクチャーをサポートすることを計画している場合はお勧めできませんが、おそらくそうすることができます。

interjayによって与えられた標準的な引用はUBを示す良いものですが、それはポインター-ポインター算術を扱うので、私の意見では2番目に良いヒットにすぎません(面白いことに、一方は明示的にUBで、もう一方はそうではありません)。質問の操作を直接扱っている段落があります:

[expr.post.incr] / [expr.pre.incr]
オペランドは[...]または完全に定義されたオブジェクトタイプへのポインタです。

ああ、ちょっと待って、完全に定義されたオブジェクトタイプですか?それで全部です?つまり、本当にタイプしますか?それで、オブジェクトはまったく必要ないのですか?
そこに何かが明確に定義されていないかもしれないというヒントを実際に見つけるには、かなりの読書が必要です。これまでのところ、完全に許可されているかのように読み取られているため、制限はありません。

[basic.compound] 31つのポインタのタイプについて説明し、他の3つのいずれでもない場合、操作の結果は明らかに3.4:invalid pointerに分類されます
ただし、無効なポインタを使用することは許可されていません。それどころか、ポインタが定期的に無効になる非常に一般的な通常の条件(たとえば、ストレージ期間の終了)がリストされています。つまり、それは明らかに許容できることです。本当に:

[basic.stc] 4
無効なポインター値を介した間接化および無効なポインター値を割り当て解除関数に渡すと、動作が未定義になります。無効なポインタ値を他の方法で使用すると、実装で定義された動作になります。

そこでは「その他」を実行しているため、未定義の動作ではなく、実装によって定義されるため、一般的には許可されます(実装が明示的に別のことを述べていない限り)。

残念ながら、これで話は終わりではありません。最終的な結果はこれ以降変更されませんが、「ポインタ」を検索する時間が長くなるほど、混乱が生じます。

[basic.compound]
オブジェクトポインタ型の有効な値は、メモリ内のバイトアドレスまたはnullポインタを表します。タイプTのオブジェクトがアドレスAにある場合、[...]は、値の取得方法関係なく、そのオブジェクトを指しているといいます。
[注:たとえば、配列の終わりを過ぎたアドレスは、そのアドレスにある可能性のある、配列の要素タイプの無関係なオブジェクトを指していると見なされます。[...]]。

次のように読みます。ポインターがメモリ内のどこかを指している限り、私は元気ですか?

[basic.stc.dynamic.safety]ポインター値は安全に派生したポインターです[何とか]

読み方:OK、安全に派生したものは何でも。これが何であるかを説明していませんし、私が実際にそれを必要としているとは言いません。安全に派生したもの。どうやら、私はまだ安全に派生していないポインタをうまく使うことができます。それらを逆参照することはおそらくそれほど良い考えではないと思いますが、それらを使用することは完全に許容されます。それはそうではありません。

実装では、ポインタの安全性が緩和されている場合があります。その場合、ポインタ値の有効性は、安全に派生したポインタ値であるかどうかには依存しません。

ああ、それで問題ないかもしれません。しかし、待って... "そうではない"?つまり、それそうかもしれません。どうやって知るの?

または、実装は厳密なポインター安全性を備えている場合があります。その場合、参照される完全なオブジェクトが動的ストレージ期間であり、以前に到達可能と宣言されていない限り、安全に派生したポインター値ではないポインター値は無効なポインター値です。

待ってください、それで私declare_reachable()はすべてのポインターを呼び出す必要がある可能性さえありますか?どうやって知るの?

これで、intptr_t明確に定義されたに変換でき、安全に派生したポインタの整数表現を提供します。もちろん、これは整数なので、自由にインクリメントすることは完全に正当で明確です。
そして、はい、あなたはintptr_t戻ってポインタも変換できます。これもまた明確に定義されています。元の値ではないだけで、安全な派生ポインタがあることは保証されません(明らかに)。それでも、全体として、標準の文字通り、実装で定義されていますが、これは100%正当なことです。

[expr.reinterpret.cast] 5
整数型または列挙型の値は、明示的にポインターに変換できます。十分なサイズの整数に変換されたポインター[...]と同じポインター型[...]の元の値に戻るポインター。ポインターと整数の間のマッピングは、それ以外の場合は実装定義です。

キャッチ

ポインターは単なる通常の整数であり、たまたまそれらをポインターとして使用します。ああ、それが本当なら!
残念ながら、それがまったく当てはまらないアーキテクチャが存在し、無効なポインタを生成するだけで(逆参照せず、ポインタレジスタに格納するだけで)トラップが発生します。

それが「実装定義」のベースです。それ、あなたはいつでもあなたがしてくださいと、ポインタをインクリメントしているという事実でし標準に対処する必要はありませんもちろん原因のオーバーフローの。アプリケーションのアドレス空間の終わりはオーバーフローの場所と一致しない可能性があり、特定のアーキテクチャー上のポインターのオーバーフローなどが存在するかどうかさえわかりません。全体としては、それは悪夢のような混乱であり、可能な利益とは関係ありません。

一方、過去のオブジェクトの状態を処理するのは簡単です。実装では、オブジェクトが割り当てられていないことを確認するだけで、アドレス空間の最後のバイトが占有されます。保証するのに便利で簡単なので、明確に定義されています。


1
あなたの論理には欠陥があります。「では、オブジェクトはまったく必要ないのですか?」単一のルールに焦点を当てることにより、標準を誤って解釈します。その規則は、プログラムが整形式であるかどうかにかかわらず、コンパイル時間に関するものです。実行時間に関する別のルールがあります。実行時にのみ、実際に特定のアドレスにあるオブジェクトの存在について話すことができます。プログラムはすべてのルールを満たす必要があります。コンパイル時のコンパイル時ルールと実行時のランタイムルール。
MSalters

5
「OK、だれが気にする!ポインタがメモリ内のどこかを指している限り、私は大丈夫ですか?」と同様の論理上の欠陥があります。いいえ。すべてのルールに従う必要があります。「ある配列の終わりが別の配列の始まりである」という難しい言語は、メモリを連続的に割り当てるための実装許可を与えるだけです。割り当て間で空き領域を維持する必要はありません。つまり、ある配列オブジェクトの終わりと別の配列オブジェクトの始まりの両方として、コードに同じ値Aが含まれる可能性があります
MSalters

1
「トラップ」は、「実装定義」の動作で説明できるものではありません。interjayが+演算子(++フロー元)の制限を検出したことに注意してください。これは、「終了後1つ」の後ろを指すことが定義されていないことを意味します。
Martin Bonnerはモニカ

1
@PeterCordes:basic.stcの第4段落をお読みください。「インダイレクション[...]未定義の動作。無効なポインタ値のその他の使用には、実装定義の動作があります」と書かれています。私はその用語を別の意味で使用することで人々を混乱させていません。正確な言葉遣いです。未定義の動作ではありません
デイモン

2
ポストインクリメントの抜け穴を見つけた可能性はほとんどありませんが、ポストインクリメントの機能に関する完全なセクションを引用していません。今は自分で調べません。ある場合は意図しないものであることに同意します。とにかく、ISO C ++がフラットメモリモデル(MaximEgorushkin)に対してより多くのものを定義した場合と同じように、任意のものを許可しない他の理由(ポインターラップアラウンドなど)があります。64ビットx86ではポインター比較を符号付きまたは符号なしのどちら
Peter Cordes
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.