添字を使用して、末尾の 1 つ前の配列要素のアドレスを取得します。C++ 標準では合法かどうか?
-
13-09-2019 - |
質問
次のコードは C++ 標準で許可されていないと主張されているのを何度か見てきました。
int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];
は &array[5]
この文脈では合法な C++ コードでしょうか?
できれば規格を参考にしてお答えいただきたいと思います。
それが C 標準を満たしているかどうかを知ることも興味深いでしょう。また、それが標準の C++ ではない場合、なぜそれを標準 C++ とは異なる扱いにする決定がなされたのでしょうか。 array + 5
または &array[4] + 1
?
解決
あなたの例は合法ですが、それは実際に範囲外のポインタを使用していないという理由だけです。
まず、範囲外のポインターを処理しましょう (例では代わりに 1 つ前のポインターが使用されていることに気づく前に、私は最初にあなたの質問をそのように解釈したためです)。
一般に、以下のことは許可されていません 作成する 範囲外のポインタ。ポインタは配列内の要素を指している必要があります。 終わりを一つ過ぎた. 。他にはどこにもありません。
ポインタは存在することさえ許可されていません。つまり、ポインタを逆参照することも明らかに許可されていません。
この件に関して規格は次のように述べています。
5.7:5:
整数を持つ式が type が ポインタの場合、結果の型は ポインター・オペランド。ポインタが オペランドは、 array オブジェクトで、配列が大きい enoughの場合、結果は 元の要素からのオフセット 要素に 結果の添え字と 元の配列要素は、 整式。要するにあの 式 P が i 番目の 要素がある場合、 式 (P)+N (同等、 N+(P)) と (P)-N (ここで、N は 値 n) は、それぞれ i+n 番目の要素と i−n 番目の要素 array オブジェクトが存在する場合は、それら。また、式Pが指す 配列の最後の要素まで オブジェクトの場合、式 (P)+1 ポイント 配列の最後の要素の 1 つ後 オブジェクトで、式 Q が 配列の最後の要素の 1 つ後 オブジェクトの場合、式 (Q)-1 は 配列オブジェクトの最後の要素。ポインターオペランドと result は、同じ array オブジェクト、または最後の 1 つ後 要素がある場合、 評価は、 オーバーフロー; それ以外の場合、動作は未解決のものです.
(私のことを強調)
もちろん、これは演算子+用です。念のため、標準では配列の添え字について次のように述べています。
5.2.1:1:
表現
E1[E2]
(定義上) と同一です*((E1)+(E2))
もちろん、明らかな注意点があります。あなたの例では、実際には境界外のポインターは表示されません。それは「終わりを 1 つ過ぎた」ポインターを使用しますが、これは異なります。(上記のように) ポインターの存在は許可されていますが、私が見る限り、標準では逆参照については何も述べられていません。私が見つけた中で最も近いのは 3.9.2:3 です。
[注記:たとえば、配列の末尾(5.7)を1つ過ぎたアドレスは、 そのアドレスにある可能性のある配列の要素型の無関係なオブジェクトを指します。—終了注記]
これは、はい、合法的に参照を解除できますが、その場所への読み取りまたは書き込みの結果は不特定であることを意味しているように私には思えます。
ここの最後の部分を修正し、質問の最後の部分に答えてくれた ilproxyil に感謝します。
array + 5
実際にはそうではありません 何でも逆参照し、単に 末尾の 1 つ後のポインターを作成します のarray
.&array[4] + 1
逆参照array+4
(これは完全に安全です)、 その左辺値のアドレスを取り、 そのアドレスに 1 を追加します。 結果は、終了時の 1 つのポインターになります (しかし、そのポインタは決して取得しません 逆参照されます。&array[5]
配列+5 を逆参照 (私が見る限り、これは合法です。 そして、"無関係なオブジェクト of the array's element type」を、 上記で述べた)、次に その要素のアドレスで、 十分に合法のようです。
したがって、まったく同じことを行うわけではありませんが、この場合、最終結果は同じになります。
他のヒント
はい、それは法的です。 C99ドラフト標準のから:
§6.5.2.1、パラグラフ2:
角括弧の
[]
における発現に続いて後置表現は添字され 配列オブジェクトの要素の指定。添字演算子[]
の定義E1[E2]
が(*((E1)+(E2)))
と同一であることです。そのため、変換ルールの+
は等価配列オブジェクト(へのポインタである場合、バイナリE1
オペレータに適用 初期配列オブジェクトの要素)とE2
は整数、E1[E2]
はE2
番目を指定します (ゼロから数えて)E1
の要素。
§6.5.3.2、パラグラフ3(強調鉱山):
単項
&
演算子は、そのオペランドのアドレスを生成します。オペランドが型を持つ場合「『の種類の』」、 結果は「」 『のの型へのポインタ』を入力しています。オペランドが単項*
演算子の結果である場合には、 そのオペレータも&
演算子のいずれもが評価され、両方であるかのように結果があります オペレーターの制約がまだ適用されことを除いて、省略され、その結果ではありません 左辺値。オペランドが[]
演算子、いずれも&演算子の結果でも*
によって暗示される単項[]
を評価し、結果が&
オペレータかのようである場合も同様に、<強いです> 除去し、[]
演算子は+
演算するに変更しました。そうでなければ、結果は そのオペランドで指定されたオブジェクトまたは関数へのポインタ。
§6.5.6、パラグラフ8:
整数型を持つ式は、ポインタに加算または減算されると、 結果は、ポインタオペランドの型を持っています。の要素へのポインタオペランド点もし 配列オブジェクト、およびアレイは、十分からオフセット要素に結果ポイント大きいです そのような元の要素もたらし、元の添字の差 配列要素は整数式に等しいです。言い換えれば、式はにポイントを
P
場合 配列オブジェクトのi
番目の要素、式の(P)+N
(等価的に、N+(P)
)と(P)-N
(N
が値n
を有している)、それぞれのi+n
番目とi−n
番目の要素を指し 配列オブジェクトは、それらが存在しました。また、発現は、最後にポイントをP
場合 配列オブジェクトの要素、表現(P)+1
ポイントの最後の要素過去1 配列オブジェクト、および発現Q
点もし配列オブジェクトの最後の要素過去1、 発現は、配列オブジェクトの最後の要素を指し(Q)-1
。両方のポインタの場合 オペランドと同じ配列オブジェクトの要素を結果ポイント、または最後の過去1 配列オブジェクトの要素は、評価はオーバーフローを生成してはなりません。そうでない場合は、 動作は未定義です。結果は配列オブジェクトの最後の要素を過ぎて1を指している場合、それを 評価される単項*
演算子のオペランドとして使用してはならない。
標準が明示的にポインタが配列の最後を過ぎて一つの要素を指すようにできることに注意してください、は、彼らが間接参照されたのないことを条件とします。 6.5.2.1及び6.5.3.2によって、発現&array[5]
は、アレイの端過去1ポイント&*(array + 5)
、と等価である(array+5)
、と等価です。これは、(6.5.3.2によって)間接参照にはなりませんので、それが合法である。
それ は 法律上の。
C++ の gcc ドキュメントによると, &array[5]
合法です。どちらの C++ でも そしてCでは 配列の終わりの 1 つ先の要素を安全にアドレス指定できます。有効なポインタが得られます。それで &array[5]
表現としては合法です。
ただし、ポインタが有効なアドレスを指している場合でも、未割り当てメモリへのポインタを逆参照しようとする動作は未定義です。したがって、その式によって生成されたポインタを逆参照しようとすることは、まだ未定義の動作です(つまり、不正)、ポインタ自体が有効であっても。
ただし、実際には、通常はクラッシュを引き起こすことはないと思います。
編集:ちなみに、これは通常、STL コンテナの end() イテレータが (最後を 1 つ通過するポインタとして) 実装される方法なので、これはこの慣行が合法であることのかなり良い証拠です。
編集:ああ、あなたが実際に尋ねているのは、そのアドレスへのポインタを保持することが合法かどうかではなく、ポインタを取得するその正確な方法が合法かどうかを尋ねていることがわかります。それについては他の回答者に任せます。
私は、これが合法であることを信じて、それが行われている「左辺値右辺値への」変換に依存します。最後の行問題の核心は、 232 の次があります
私たちは、標準でのアプローチは大丈夫と思われることに同意した:P = 0; * P;本質的にエラーではありません。左辺値ツー右辺値の変換はそれを未定義の動作を与えるだろう。
これはわずかに異なる例ではあるが、何それはショーを行うことは「*」右辺値への変換左辺値になるとしないということであるので、式は、その後の行動を左辺値を期待「&」の即値オペランドであることを考えます定義されています。
それが違法だとは思いませんが、&array[5] の動作は未定義だと思います。
5.2.1 [expr.sub] E1[E2] は (定義により) *((E1)+(E2)) と同一です
5.3.1 [expr.unary.op] 単項 * 演算子 ...結果は、式が指すオブジェクトまたは関数を参照する左辺値になります。
この時点では、式 ((E1)+(E2)) が実際にはオブジェクトを指しておらず、実際にはオブジェクトが指されていない限り、結果がどうなるかは標準で規定されているため、未定義の動作になります。
- 1.3.12 [defns.unknown] この国際規格が動作の明示的な定義の記述を省略している場合にも、未定義の動作が予想される可能性があります。
他の場所で指摘されているように、 array + 5
そして &array[0] + 5
これらは、配列の末尾の 1 つ先のポインターを取得する有効で明確に定義された方法です。
上記の回答に加えて、私はオペレータを指摘するだろう&クラスでオーバーライドすることができます。それはのPODに対して有効であってもそう、おそらく、あなたが知っているオブジェクトのために行うには良いアイデアではありません(そもそも演算子&()をオーバーライド似)は有効ではありません。
これは合法です:
int array[5];
int *array_begin = &array[0];
int *array_end = &array[5];
セクション 5.2.1 添字 式 E1[E2] は (定義により) *((E1)+(E2)) と同じです
したがって、これによって、 array_end も同等であると言えます。
int *array_end = &(*((array) + 5)); // or &(*(array + 5))
セクション 5.3.1.1 単項演算子「*」:単項 * 演算子は間接演算を実行します。適用される式は、オブジェクト型へのポインターでなければなりません。 関数型へのポインタと 結果はオブジェクトまたは関数を参照する左辺値です 式が指すもの。式の型が "T へのポインター" の場合、結果の型は "T" です。[ 注:不完全な型へのポインター (他の CV voidより)を逆参照できます。このようにして得られた左辺値は、限られた方法で使用できます (参照を初期化するには、 例);この左辺値は右辺値に変換してはなりません。4.1 を参照してください。— 終了ノート]
上記の重要な部分:
「結果は、オブジェクトまたは関数を参照する左辺値です。」
単項演算子 '*' は、int を参照する左辺値を返します (逆参照なし)。次に、単項演算子「&」は左辺値のアドレスを取得します。
範囲外のポインタの逆参照がない限り、その操作は標準で完全にカバーされており、すべての動作が定義されています。したがって、私が読んだ限りでは、上記は完全に合法です。
STL アルゴリズムの多くが明確に定義された動作に依存しているという事実は、標準委員会がこのことについてすでに考えているという一種のヒントであり、これを明示的にカバーする何かがあると私は確信しています。
以下のコメント セクションには 2 つの引数が示されています。
(読んでください:でも長いし、結局二人とも荒らしちゃうよ)
引数 1
これはセクション 5.7 の第 5 項により違法です。
整数型の式をポインターに加算またはポインターから減算すると、結果はポインター オペランドの型になります。ポインタ オペランドが配列オブジェクトの要素を指しており、その配列が十分大きい場合、結果は、結果の配列要素と元の配列要素の添え字の差が整数式と等しくなるように、元の要素からオフセットされた要素を指します。つまり、式 P が配列オブジェクトの i 番目の要素を指している場合、式 (P)+N (N+(P) と同等) および (P)-N (N の値は n) は、式 P が配列オブジェクトの i 番目の要素を指していることを示します。配列オブジェクトの i + n 番目および i − n 番目の要素 (存在する場合) にそれぞれ追加されます。さらに、式 P が配列オブジェクトの最後の要素を指す場合、式 (P)+1 は配列オブジェクトの最後の要素の 1 つ先を指し、式 Q が配列オブジェクトの最後の要素の 1 つ先を指す場合、式 (Q)-1 は、配列オブジェクトの最後の要素を指します。ポインター・オペランドと結果の両方が、同じ配列オブジェクトのエレメントを指す場合、または 1 つ前のエレメントを指す場合 配列オブジェクトの最後の要素の場合、評価はオーバーフローを生成しません。それ以外の場合、動作は未定義です。
このセクションは関連性がありますが、未定義の動作は示されません。私たちが話している配列内のすべての要素は、配列内か、末尾の 1 つ先 (上の段落で明確に定義されています) のいずれかにあります。
引数 2:
以下に示す 2 番目の引数は次のとおりです。 *
は逆参照演算子です。
これは「*」演算子を説明するために使用される一般的な用語ですが、「逆参照」という用語は、言語とそれが基盤となるハードウェアにとって何を意味するかに関して明確に定義されていないため、この用語は標準では意図的に避けられています。
ただし、配列の末尾を 1 つ超えてメモリにアクセスすることは、明らかに未定義の動作です。私は確信が持てません unary * operator
メモリにアクセスします (メモリへの読み取り/書き込み) この文脈では (標準で定義されている方法ではありません)。この文脈では (標準で定義されているように (5.3.1.1 を参照)、 unary * operator
を返します lvalue referring to the object
. 。私の言語理解では、これは基礎となる記憶へのアクセスではありません。この式の結果はすぐに使用されます。 unary & operator
によって参照されるオブジェクトのアドレスを返す演算子 lvalue referring to the object
.
他にも、Wikipedia や非正規の情報源への多くの参照が示されています。どれも私には無関係だと思います。 C++は標準で定義されています.
結論:
標準には私が考慮していない部分が多く、上記の議論が間違っていることが証明される可能性があることは認めたいと思います。 のん 以下に提供されます。標準的なリファレンスを見せてもらうと、これが UB であることがわかります。私はします
- 答えはそのままにしておきます。
- すべて大文字で書くと、これは愚かであり、誰もが読むのは間違っています。
これは議論ではありません:
世界中のすべてが C++ 標準で定義されているわけではありません。心を開いてください。
ワーキングドラフト( n2798 ):
「単項&演算子の結果です そのオペランドへのポインタ。オペランド 左辺値またはクアリFi回線ED-idをしなければなりません。 の第1の場合において、もしタイプ 表現は、「T」のタイプです 結果「が「Tへのポインタ」である(p。103)
配列[5]私が言うことができる最善のように修飾-IDではありません(リストがpである87。);最も近い識別子であるように見えるが、配列識別子の配列であるが[5]ではないであろう。 「左辺値は、オブジェクトまたは関数を意味する。」ので左辺値ではない(p。76)。アレイは、[5]、明らかに機能しないであり、かつ(配列+ 5は、最後に割り当てられた配列要素の後にあるため)有効なオブジェクトを参照することが保証されていない。
もちろん、それは特定のケースで働くかもしれないが、それは、有効なC ++または安全ではありません。
注:これは、配列、過去1を取得するために追加することが合法である(P 113):
「IF式P [ポインタ] 配列の最後の要素を指し オブジェクト、式(P)+1点 配列の最後の要素過去1 オブジェクト、およびif式のQポイント 配列の最後の要素過去1 オブジェクト、式(Q)-1点に 配列オブジェクトの最後の要素。 ポインタオペランドとの両方の場合 同一の要素に結果ポイント 配列オブジェクト、または最後の過去1 配列オブジェクトの要素、 評価は得られないもの 以上の流動 "
しかし、それは&使用してそうする法的ではありません。
それが合法であるとしても、なぜ慣習から逸脱?配列+ 5はとにかく短く、私の意見では、より読みます。
編集:あなたは、対称によってそれをしたい場合は、あなたが書くことができます。
int* array_begin = array;
int* array_end = array + 5;
次の理由により、これは未定義の動作である必要があります。
範囲外の要素にアクセスしようとすると、未定義の動作が発生します。したがって、標準はその場合に例外をスローする実装を禁止していません (つまり、要素にアクセスする前に実装が境界をチェックします)。もし
& (array[size])
と定義されていましたbegin (array) + size
, 、境界外アクセスの場合に例外をスローする実装は、もはや標準に準拠していません。この収量を出すのは不可能です
end (array)
array が配列ではなく、任意のコレクション型である場合。
C ++標準、5.19、パラグラフ4:
アドレス定数式は、左辺値へのポインタである....ポインタは、単項&演算子を使用して、明示的に作成...またはアレイ(4.2)の式を用いなければならない...種類。添字演算子は、[] ...アドレス定数式の作成に使用することができるが、オブジェクトの値は、これらの演算子を使用してアクセスしてはなりません。添字演算子を使用する場合は、そのオペランドの一方は整数定数式でなければならない。
私には見えるが好き&配列[5]アドレス定数式であること、有効なC ++です。
あなたの例は、一般的なケースが、特定のものでない場合、それは許されます。あなたはは法的にの、私の知る限り、メモリの割り当てられたブロック過去1を移動することができます。 それはあなたが配列の末尾から1で遠くの要素にアクセスしようとしているすなわちしかし、一般的なケースでは動作しません。
ただ、検索C-FAQます:
のhref="http://c-faq.com/aryptr/non0based.html" rel="nofollow noreferrer">リンクテキストこれは完全に合法です。
あなたがmyVec.end()を呼び出すときにSTLからベクトル<>テンプレートクラスはまさにこのん:それはあなたの配列の最後を過ぎて一つの要素を指す(ここでは、イテレータのような)ポインタを取得します。
。