web-dev-qa-db-ja.com

C ++ 17以降、正しいアドレスとタイプのポインターは常に有効なポインターですか?

この質問と回答 を参照してください。)

C++ 17標準より前は、次の文が [basic.compound]/ に含まれていました。

タイプTのオブジェクトがアドレスAにある場合、値が取得された方法に関係なく、値がアドレスAであるタイプcv T *のポインターはそのオブジェクトを指していると言われます。

しかし、C++ 17以降、この文は 削除 になっています。

たとえば、この文によってこのサンプルコードが定義され、C++ 17以降ではこれは未定義の動作であると考えられます。

_ alignas(int) unsigned char buffer[2*sizeof(int)];
 auto p1=new(buffer) int{};
 auto p2=new(p1+1) int{};
 *(p1+1)=10;
_

C++ 17より前は、_p1+1_は_*p2_へのアドレスを保持し、正しい型を持っているため、*(p1+1)は_*p2_へのポインターです。 C++ 17では、_p1+1_は ポインター過去の終わり であるため、オブジェクトへのポインター、それは逆参照可能ではないと思います。

この標準的権利の変更のこの解釈は、引用文の削除を補う他の規則がありますか?

80
Oliv

この標準的権利の変更のこの解釈は、この引用文の削除を補う他の規則がありますか?

はい、この解釈は正しいです。末尾を過ぎたポインターは、そのアドレスを指す別のポインター値に単純に変換できません。

新しい [basic.compound]/ の意味:

ポインター型のすべての値は次のいずれかです。
(3.1)オブジェクトまたは関数へのポインター(ポインターはオブジェクトまたは関数を指していると言われます)、または
(3.2)オブジェクトの終わりを過ぎたポインター([expr.add])、または

これらは相互に排他的です。 _p1+1_は、オブジェクトへのポインタではなく、末尾を過ぎたポインタです。 _p1+1_は、_x[1]_ではなく、_p1_にあるサイズ1配列の架空の_p2_を指します。これらの2つのオブジェクトは、ポインターを相互変換できません。

非規範的なメモもあります:

[注:オブジェクトの終わりを過ぎたポインター([expr.add])は、そのアドレスにある可能性のあるオブジェクトタイプの無関係なオブジェクトを指すとは見なされません。 [...]

意図を明確にします。


T.C.として多数のコメントで指摘しています( 特にこれ )、これは実際に_std::vector_を実装しようとすることに伴う問題の特別なケースです-[v.data(), v.data() + v.size())が必要なことです有効な範囲であり、しかもvectorは配列オブジェクトを作成しないため、定義されたポインター演算は、ベクトル内の任意のオブジェクトから仮想の1サイズの配列の終わりまでになります。その他のリソースについては、 CWG 2182このstdディスカッション 、およびこの件に関する論文の2つの改訂版 P0593R および P0593R1 (特にセクション1.3)。

44
Barry

あなたの例では、*(p1 + 1) = 10;はUBである必要があります。サイズが1の配列の最後の1つであるためです。しかし、配列は動的なため、より大きなchar配列で構築されます。

動的オブジェクトの作成については、C++標準のn4659ドラフトの§4.5 The C++ object model [intro.object]、§3で説明されています。

3タイプ「N unsigned charの配列」またはタイプ「Nの配列std :: byte」(21.2.1)の別のオブジェクトeに関連付けられたストレージに完全なオブジェクトが作成された場合(8.3.4)、その配列はストレージを提供します作成されたオブジェクトの場合:
(3.1)— eのライフタイムが開始されており、終了していません。
(3.2)—新しいオブジェクトのストレージはeに完全に収まり、
(3.3)—これらの制約を満たす小さな配列オブジェクトはありません。

3.3はかなり不明瞭に見えますが、以下の例で意図をより明確にします。

_struct A { unsigned char a[32]; };
struct B { unsigned char b[16]; };
A a;
B *b = new (a.a + 8) B; // a.a provides storage for *b
int *p = new (b->b + 4) int; // b->b provides storage for *p
// a.a does not provide storage for *p (directly),
// but *p is nested within a (see below)
_

そのため、例では、buffer配列はストレージが_*p1_と_*p2_の両方に対応しています。

次の段落は、_*p1_と_*p2_の両方の完全なオブジェクトがbufferであることを証明しています。

4次の場合、オブジェクトaは別のオブジェクトb内にネストされます。
(4.1)— aはbのサブオブジェクト、または
(4.2)— bはaのストレージを提供します、または
(4.3)— aがc内にネストされ、cがb内にネストされているオブジェクトcが存在します。

5すべてのオブジェクトxには、次のように決定されるxの完全なオブジェクトと呼ばれるオブジェクトがあります。
(5.1)— xが完全なオブジェクトである場合、xの完全なオブジェクトはそれ自体です。
(5.2)—それ以外の場合、xの完全なオブジェクトは、xを含む(一意の)オブジェクトの完全なオブジェクトです。

これが確立されると、C++ 17のドラフトn4659のその他の関連部分は[basic.coumpound]§3(私のものを強調)です。

3 ...ポインタ型のすべての値は次のいずれかです。
(3.1)—オブジェクトまたは関数へのポインター(ポインターはオブジェクトまたは関数を指していると言われます)、または
(3.2)—オブジェクトの終わりを過ぎるポインター(8.7)、または
(3.3)—そのタイプのNULLポインター値(7.11)、または
(3.4)—無効なポインター値。

オブジェクトの終わりへの、または過ぎたポインタであるポインタ型の値は、オブジェクトが占有するメモリの最初のバイト(4.4)のアドレスを表しますまたはストレージの終わりの後のメモリの最初のバイトオブジェクトによってそれぞれ占有されます。 [注:オブジェクトの終わり(8.7)を過ぎたポインターは、そのアドレスにある可能性のあるオブジェクトタイプの無関係オブジェクトを指すとは見なされません。ポインター値は、それが示すストレージがストレージ期間の終わりに達すると無効になります。 6.7を参照してください。 —end note]ポインター演算(8.7)および比較(8.9、8.10)の目的で、n個のエレメントの配列xの最後のエレメントの終わりを過ぎたポインターは、仮想エレメントx [へのポインターと同等と見なされますn]。ポインター型の値表現は実装定義です。レイアウト互換型へのポインタは、同じ値表現と整列要件を持たなければならない(6.11)...

メモ末尾を過ぎたポインター...はここでは適用されません。なぜなら、オブジェクトは_p1_および_p2_によってポイントされ、無関係ではなく、ネストされているためです同じ完全なオブジェクトに変換されるため、ポインタ演算はストレージを提供するオブジェクト内で意味があります。_p2 - p1_が定義され、_(&buffer[sizeof(int)] - buffer]) / sizeof(int)_が1です。

したがって、_p1 + 1_ isは_*p2_へのポインターであり、*(p1 + 1) = 10;は動作を定義し、_*p2_の値を設定します。


また、C++ 14と現在の(C++ 17)標準との互換性に関するC4付録も読みました。単一の文字配列で動的に作成されたオブジェクト間でポインター演算を使用する可能性を削除することは、IMHOが一般的に使用される機能であるため、そこに引用すべき重要な変更です。互換性のページには何も存在しないので、禁止することは規格の意図ではないことを確認していると思います。

特に、デフォルトのコンストラクタを持たないクラスからのオブジェクトの配列の一般的な動的構築を無効にします。

_class T {
    ...
    public T(U initialization) {
        ...
    }
};
...
unsigned char *mem = new unsigned char[N * sizeof(T)];
T * arr = reinterpret_cast<T*>(mem); // See the array as an array of N T
for (i=0; i<N; i++) {
    U u(...);
    new(arr + i) T(u);
}
_

arrは、配列の最初の要素へのポインタとして使用できます...

8
Serge Ballesta

ここで与えられた答えを拡大することは、改訂された文言が除外していると私が信じるものの例です:

警告:未定義の動作

#include <iostream>
int main() {
    int A[1]{7};
    int B[1]{10};
    bool same{(B)==(A+1)};

    std::cout<<B<< ' '<< A <<' '<<sizeof(*A)<<'\n';
    std::cout<<(same?"same":"not same")<<'\n';
    std::cout<<*(A+1)<<'\n';//!!!!!  
    return 0;
}

実装に完全に依存する(および脆弱な)理由で、このプログラムの出力は次のとおりです。

0x7fff1e4f2a64 0x7fff1e4f2a60 4
same
10

この出力は、2つの配列(その場合)がメモリに保存され、Aの「終わりを過ぎたもの」がB

改訂された仕様では、A+1Bへの有効なポインターになることはありません。 「値の取得方法に関係なく」という古いフレーズは、「A + 1」が「B [0]」を指す場合、「B [0]」への有効なポインターであることを示しています。それは良いことではなく、意図することは決してありません。

1
Persixty