24

次のコード (サブオブジェクトの境界を越えてポインター演算を実行する) はT、コンパイル対象の型 (C++11 では必ずしも POD である必要はありません) またはそのサブセットに対して明確に定義された動作を持っていますか?

#include <cassert>
#include <cstddef>

template<typename T>
struct Base
{
    // ensure alignment
    union
    {
        T initial;
        char begin;
    };
};

template<typename T, size_t N>
struct Derived : public Base<T>
{
    T rest[N - 1];
    char end;
};

int main()
{
    Derived<float, 10> d;
    assert(&d.rest[9] - &d.initial == 10);
    assert(&d.end - &d.begin == sizeof(float) * 10);
    return 0;
}

LLVM は、最初は小さな配列にスタックを使用するように最適化されていますが、初期容量を超えるとヒープ割り当てバッファーに切り替える内部ベクター型の実装で、上記の手法のバリエーションを使用します。(このようにする理由は、この例からは明らかではありませんが、明らかにテンプレート コードの肥大化を減らすためです。これは、コードを調べればより明確になります。)

注:誰かが文句を言う前に、これはまさに彼らが行っていることではなく、彼らのアプローチは私がここで述べたものよりも標準に準拠している可能性がありますが、一般的なケースについてお尋ねしたいと思います.

明らかに、それは実際には機能しますが、標準の何かがそうであることを保証するかどうか興味があります. N3242/expr.addを考えると、私はノーと言う傾向があります:

同じ配列オブジェクトの要素への 2 つのポインターを減算すると、結果は 2 つの配列要素の添字の差になります...さらに、式 P が配列オブジェクトの要素または最後の要素の 1 つ後ろを指している場合式 Q が同じ配列オブジェクトの最後の要素を指している場合、式 ((Q)+1)-(P) は ((Q)-(P))+1 と同じ値を持ち、 -((P)-((Q)+1)) のように、式 P が配列オブジェクトの最後の要素の 1 つ後ろを指している場合、式 (Q)+1 がオブジェクトを指していなくても、値はゼロになります。配列オブジェクトの要素。...両方のポインターが同じ配列オブジェクトの要素、または配列オブジェクトの最後の要素の 1 つ後ろを指していない限り、動作は未定義です。

しかし、理論的には、上記の引用の中間部分と、クラス レイアウトおよびアライメントの保証を組み合わせると、次の (マイナーな) 調整が有効になる可能性があります。

#include <cassert>
#include <cstddef>

template<typename T>
struct Base
{
    T initial[1];
};

template<typename T, size_t N>
struct Derived : public Base<T>
{
    T rest[N - 1];
};

int main()
{
    Derived<float, 10> d;
    assert(&d.rest[9] - &d.rest[0] == 9);
    assert(&d.rest[0] == &d.initial[1]);
    assert(&d.rest[0] - &d.initial[0] == 1);
    return 0;
}

unionこれは、レイアウト、 との間の変換可能性などに関する他のさまざまな規定と組み合わせるchar *ことで、元のコードも同様に有効になる可能性があります。(主な問題は、上記のポインター演算の定義に推移性がないことです。)

誰でも確かに知っていますか?N3242/expr.addは、ポインターが定義されるためには、ポインターが同じ「配列オブジェクト」に属している必要があることを明確にしているようですが、標準の他の保証が組み合わされた場合、とにかく定義が必要になる可能性があるという仮説が立てられます。この場合、論理的に一貫性を保つためです。(私はそれに賭けているわけではありませんが、少なくとも考えられると思います。)

EDIT : @MatthieuM は、このクラスは標準レイアウトではないため、基本サブオブジェクトと派生の最初のメンバーの間にパディングが含まれていないことが保証されない可能性があるという異議を唱えますalignof(T)。それがどれほど正しいかはわかりませんが、次のさまざまな質問が開かれます。

  • 継承が削除された場合、これは機能することが保証されますか?

  • &d.end - &d.begin >= sizeof(float) * 10そうでなくても保証されますか&d.end - &d.begin == sizeof(float) * 10

最終編集@ArneMertz は、 N3242/expr.addを非常によく読むことを主張しています (はい、ドラフトを読んでいることは知っていますが、十分に近いです)。ただし、標準は、次の動作が未定義であることを実際に暗示していますか?行が削除されますか?(上記と同じクラス定義)

int main()
{
    Derived<float, 10> d;
    bool aligned;
    float * p = &d.initial[0], * q = &d.rest[0];

    ++p;
    if((aligned = (p == q)))
    {
        std::swap(p, q); // does it matter if this line is removed?
        *++p = 1.0;
    }

    assert(!aligned || d.rest[1] == 1.0);

    return 0;
}

また、 if==が十分に強くない場合はstd::less、ポインターに対して全順序を形成するという事実を利用して、上記の条件を次のように変更するとどうなるでしょうか。

    if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p))))

2 つの等しいポインターが同じ配列オブジェクトを指していると想定するコードは、標準の厳密な読み取りによれば本当に壊れているのでしょうか?

編集申し訳ありませんが、標準のレイアウトの問題を解消するために、もう1つの例を追加したいだけです:

#include <cassert>
#include <cstddef>
#include <utility>
#include <functional>

// standard layout
struct Base
{
    float initial[1];
    float rest[9];
};

int main()
{
    Base b;
    bool aligned;
    float * p = &b.initial[0], * q = &b.rest[0];

    ++p;
    if((aligned = (p == q)))
    {
        std::swap(p, q); // does it matter if this line is removed?
        *++p = 1.0;
        q = &b.rest[1];
        // std::swap(p, q); // does it matter if this line is added?
        p -= 2; // is this UB?
    }
    assert(!aligned || b.rest[1] == 1.0);
    assert(p == &b.initial[0]);

    return 0;
}
4

1 に答える 1

9

更新:この回答は最初は一部の情報を見逃していたため、誤った結論につながりました。

あなたの例では、initialそしてrest明らかに異なる(配列)オブジェクトなので、initial(またはその要素)へのポインタをrest(またはその要素)へのポインタと比較することは

  • UB、ポインタの違いを使用する場合。(§5.7,6)
  • 関係演算子を使用する場合は指定なし(§5.9,2)
  • 明確に定義されています==(したがって、2番目の切り取りは適切です。以下を参照してください)

最初のスニペット:

最初のスニペットで違いを構築することは、あなたが提供した見積もり(§5.7,6)の未定義の動作です:

両方のポインタが同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素を1つ過ぎている場合を除いて、動作は定義されていません。

最初のサンプルコードのUB部分を明確にするには:

//first example
int main()
{
    Derived<float, 10> d;
    assert(&d.rest[9] - &d.initial == 10);            //!!! UB !!!
    assert(&d.end - &d.begin == sizeof(float) * 10);  //!!! UB !!! (*)
    return 0;
}

でマークされた行(*)は興味深いものです。d.begind.endは同じ配列の要素ではないため、操作の結果はUBになります。reinterpret_cast<char*>(&d)これは、結果の配列に両方のアドレスが含まれている可能性があるという事実にも関わらずです。ただし、その配列はのすべてを表すため、の一部へのdアクセスとは見なされません。したがって、その操作はおそらく正常に機能し、夢見ることができるすべての実装で期待される結果をもたらしますが、それでも定義の問題としてUBです。d

2番目のスニペット:

これは実際には明確に定義された動作ですが、実装によって定義された結果は次のとおりです。

int main()
{
    Derived<float, 10> d;
    assert(&d.rest[9] - &d.rest[0] == 9);
    assert(&d.rest[0] == &d.initial[1]);         //(!)
    assert(&d.initial[1] - &d.initial[0] == 1);
    return 0;
}

でマークされた行はubで(!)はありませんが、パディング、配置、および前述のインスツルメンテーションが役割を果たす可能性があるため、その結果は実装定義になります。しかしそのアサーションが成り立つ場合は、1つの配列のように2つのオブジェクト部分を使用できます

あなたはそれが記憶rest[0]の直後にあることを知っているでしょう。一見すると、平等を簡単に使用することはできませんでした。initial[0]

  • initial[1]initialUBであることを逆参照して、の最後の1つを指します。
  • rest[-1]明らかに範囲外です。

しかし、§3.9.2,3に入ります:

タイプのオブジェクトがTアドレスにある場合、値がアドレスであるタイプcvAのポインタは、値がどのように取得されたかに関係なく、そのオブジェクトを指していると言われます。[注:たとえば、配列の終わりを1つ超えたアドレス(5.7)は、そのアドレスにある可能性のある配列の要素タイプの無関係なオブジェクトを指していると見なされます。 T*A

したがって&initial[1] == &rest[0]、が1つの配列しかない場合と同じようにバイナリになり、すべて問題ありません。

境界に「ポインタコンテキストスイッチ」を適用できるため、両方の配列を反復処理できます。だからあなたの最後のスニペットに:swapは必要ありません!

ただし、いくつかの注意点があります。rest[-1]これはUBであり、 §5.7,5initial[2]のためにそうなります。

ポインタオペランドと結果の両方が同じ配列オブジェクトの要素を指している場合、または配列オブジェクトの最後の要素を1つ過ぎている場合、評価によってオーバーフローが発生することはありません。それ以外の場合、動作は未定義です。

(私の強調)。では、これら2つはどのように組み合わされますか?

  • 「適切なパス」:問題&initial[1]ありません。§3.9.2,3により、&initial[1] == &rest[0]そのアドレスを取得してポインタをインクリメントし、の他の要素にアクセスできるためです。rest
  • 「不正なパス」:initial[2]です*(initial + 2)が、§5.7,5以降、initial +2すでにUBであり、ここで§3.9.2,3を使用することはできません。

一緒に:境界に立ち寄り、少し休憩してアドレスが等しいことを確認してから、次に進む必要があります。

于 2013-03-05T10:30:42.490 に答える