次のコード (サブオブジェクトの境界を越えてポインター演算を実行する) は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;
}