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


84

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

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

タイプ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 ++で17p1+1ある過去エンドポインタになっていないので、オブジェクトへのポインタと、私はそれがdereferencableではないと信じています。

この標準的な権利の変更の解釈ですか、それとも引用された文の削除を補償する他の規則がありますか?


注:そこにポインタの出所についての新しい/更新されたルールは、[basic.stc.dynamic.safety]であり、[util.dynamic.safety]
MM

@MMこれは、(実験エラーの範囲内で)空のセットである厳密なポインターの安全性を備えた実装でのみ重要です。
TC

4
引用された声明は、実際には決して真実ではありませんでした。与えられたint a, b = 0;、あなたが*(&a + 1) = 1;チェックしたとしてもあなたはすることができません&a + 1 == &b。オブジェクトのアドレスを推測するだけでオブジェクトへの有効なポインタを取得できる場合は、ローカル変数をレジスタに格納することさえ問題になります。
TC

@TC 1)アドレスを取得した後、どのコンパイラが変数をregに入れますか?2)住所を測定せずに正しく推測するにはどうすればよいですか?
curiousguy 2018年

@curiousguyまさにそのため、他の手段(推測など)で取得した数値を、オブジェクトがたまたまあるアドレスにキャストするだけでは問題があります。そのオブジェクトをエイリアスしますが、コンパイラはそれを認識しません。対照的に、オブジェクトのアドレスを取得すると、言うとおりになります。コンパイラは警告を受け、それに応じて同期します。
ピーター-モニカを復活させる

回答:


45

この標準的な権利の変更のこの解釈ですか、それともこの引用された文の削除を補償する他の規則がありますか?

はい、この解釈は正しいです。末尾を超えたポインタは、そのアドレスを指す別のポインタ値に単純に変換できるわけではありません。

新しい[basic.compound] / 3は次のように述べています。

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

それらは相互に排他的です。p1+1オブジェクトへのポインタではなく、末尾を超えたポインタです。ではなく、でのサイズ1配列のp1+1仮説x[1]を指します。これらの2つのオブジェクトは、ポインター相互変換可能ではありません。p1p2

非規範的な注記もあります:

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

これは意図を明確にします。


TCが多数のコメント(特にこれ)で指摘しているように、これは実際には実装しようとすることに伴う問題の特殊なケースですstd::vector-つまり[v.data(), v.data() + v.size())、有効な範囲である必要vectorがありますが、配列オブジェクトを作成しないため、定義されたポインタ演算のみが、ベクトル内の任意のオブジェクトから、その仮想の1サイズ配列の最後まで進みます。その他のリソースについては、CWG 2182このstdの説明、およびこの主題に関する論文の2つの改訂版P0593R0P0593R1(具体的にはセクション1.3)を参照してください。


3
この例は基本的に、既知の「vector実装可能性の問題」の特殊なケースです。+1。
TC

2
@Oliv一般的なケースはC ++ 03以降に存在します。根本的な原因は、配列オブジェクトがないためにポインタ演算が期待どおりに機能しないことです。
TC

1
@TC私は唯一の問題はポインタ演算の制限から来ていると信じていました。この文の削除は新しい問題を追加しませんか?コード例はC ++ 17より前のUBでもありますか?
Oliv

1
@Olivポインター演算が修正されている場合、p1+1過去のポインターは生成されなくなり、過去のポインターに関する議論全体は議論の余地があります。あなたの特定の2要素の特殊なケースはUBpre-17ではないかもしれませんが、それもあまり面白くありません。
TC

5
@TCこの「ベクトルの実装可能性の問題」について読むことができる場所を教えていただけますか?
SirGuy

8

あなたの例で*(p1 + 1) = 10;は、サイズ1の配列終わりを過ぎているため、UBである必要があります。ただし、配列がより大きなchar配列で動的に構築されているため、ここでは非常に特殊なケースになります。

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

3タイプ「arrayofNunsignedchar」またはタイプ「arrayofN 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が存在する、およびcはb内にネストされています。

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)を持っている必要があります。

ノートエンド過去のAポインタ...オブジェクトはが指すので、ここでは適用されないp1p2していない無関係な、しかし、ポインタ算術演算は、ストレージを提供するオブジェクトの内部で意味をなすので、同じ完全なオブジェクトにネストされています:p2 - p1定義されていますさ(&buffer[sizeof(int)] - buffer]) / sizeof(int)つまり1です。

そうp1 + 1 ですへのポインタ*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 次に、配列の最初の要素へのポインタとして使用できます。


ああ、だから世界は夢中になっていない。+1
ストーリーテラー-UnslanderMonica18年

@StoryTeller:私も願っています。さらに、互換性のセクションでそれについて一言も言わないでください。しかし、ここでは反対意見の方が評判が高いようです...
SergeBallesta18年

2
ポインタ演算を管理する[expr.add]の規範的な規則に反して、非規範的な音符で「無関係」という1つの単語を取得し、それに耐えられない意味を与えています。一般的な場合のポインタ演算はどの標準でも機能したことがないため、付録Cには何もありません。壊すものは何もありません。
TC

3
@TC:Googleは、この「ベクトルの実装可能性の問題」に関する情報を見つけるのに非常に役立ちません。
MatthieuM.18年

6
@MatthieuM。参照してください。問題の核心2182をこのSTD-ディスカッションスレッド、P0593R0、およびP0593R1(特にセクション1.3) 。基本的な問題はvector、配列オブジェクトを作成しない(そして作成できない)が、ユーザーがポインター演算をサポートするポインター(配列オブジェクトへのポインターに対してのみ定義される)を取得できるインターフェースを持っていることです。
TC

1

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

警告:未定義の動作

#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つの配列(この場合)がメモリに格納されていることを示しています。そのため、の「終わりを過ぎた1つ」Aは、の最初の要素のアドレスの値を保持しますB

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


これはまた、構造体の最後で空の配列を使用することを効果的に禁止し、派生クラスまたはカスタムアロケーターnewがカスタムサイズの配列を指定できるようにしますか?おそらく、新しい問題は「方法に関係なく」にあります-有効な方法と危険な方法がいくつかありますか?
ジェムテイラー

@Persixtyしたがって、ポインタオブジェクトの値は、オブジェクトのバイトによって決定され、他には何も決定されません。したがって、同じ状態の2つのオブジェクトは、同じオブジェクトを指します。一方が有効な場合、もう一方も有効です。したがって、ポインタ値が数値として表される一般的なアーキテクチャでは、値が等しい2つのポインタが同じオブジェクトを指し、一方の端が同じ他のオブジェクトを指します。
curiousguy 2018

@Persixtyまた、トリビアルタイプは、タイプの可能な値を列挙できることを意味します。基本的に、最適化モードの最新のコンパイラー(-O0一部のコンパイラーでも)は、ポインターを単純な型とは見なしません。コンパイラーはstdの要件を真剣に扱いません。また、stdを作成する人々、異なる言語を夢見て、基本原則に直接矛盾するあらゆる種類の発明を行う人々も同様です。明らかに、ユーザーはコンパイラのバグについて不平を言うと混乱し、時にはひどい扱いを受けます。
curiousguy 2018

質問の非規範的なメモは、「過去の終わり」を何も指していないと考えてほしいと思っています。実際には、何かを指している可能性があり、実際にはそれを逆参照することが可能である可能性があることを私たちは知っています。しかし、それは(標準によれば)有効なプログラムではありません。私たちはすることができます想像ポインタが算術・ツー・過去・エンドによって得られた知っていて、間接参照あれば例外を発生させ実装を。私はそれを行うプラットフォームを知っていますが。規格はそれを排除したくないと思います。
Persixty 2018

@curiousguyまた、可能な値を列挙することで何を意味するのかわかりません。これは、C ++で定義されている些細なタイプの必須機能ではありません。
Persixty 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.