以下代码(跨子对象边界执行指针算术)是否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,我倾向于说不:
当两个指向同一个数组对象的元素的指针相减时,结果是两个数组元素的下标之差……此外,如果表达式 P 指向数组对象的一个元素,或者指向最后一个元素之后的一个元素一个数组对象,并且表达式 Q 指向同一个数组对象的最后一个元素,表达式 ((Q)+1)-(P) 与 ((Q)-(P))+1 具有相同的值并且为 -((P)-((Q)+1)),如果表达式 P 指向数组对象的最后一个元素后一个,则值为零,即使表达式 (Q)+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似乎明确指出,指针必须属于同一个“数组对象”才能对其进行定义,但假设标准中的其他保证在组合在一起时可能需要定义这种情况是为了保持逻辑上的自洽。(我不赌它,但我认为它至少是可以想象的。)
编辑:@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;
}
另外,如果==
不够强,如果我们利用std::less
指针上的全序这一事实,并将上面的条件更改为:
if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p))))
根据严格阅读标准,假设两个相等指针指向同一个数组对象的代码是否真的被破坏了?
编辑抱歉,只想再添加一个示例,以消除标准布局问题:
#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;
}