【发布时间】:2013-03-05 06:53:30
【问题描述】:
以下代码(跨子对象边界执行指针算术)是否对其编译的类型 T(在 C++11 中为 does not not necessarily have to be 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 在内部向量类型的实现中使用了上述技术的变体,该内部向量类型经过优化,最初将堆栈用于小型数组,但一旦超过初始容量,就会切换到堆分配的缓冲区。 (从这个例子中并不清楚这样做的原因,但显然是为了减少模板代码膨胀;如果你看一下code,这会更清楚。)
注意:在任何人抱怨之前,这并不是他们正在做的事情,可能他们的方法比我在这里给出的更符合标准,但我想问一下一般情况。
显然,它在实践中有效,但我很好奇标准中是否有任何内容可以保证这种情况。我倾向于拒绝,因为 N3242/expr.add:
当两个指向同一个数组对象的元素的指针相减时,结果是两个数组元素的下标之差……此外,如果表达式P指向数组对象的一个元素或一个过去的最后一个元素 一个数组对象,表达式 Q 指向同一数组对象的最后一个元素,表达式 ((Q)+1)-(P) 与 ((Q)-(P))+1 具有相同的值,并且-((P)-((Q)+1)),如果表达式 P 指向数组对象的最后一个元素后一个,则其值为 0,即使表达式 (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是否也能得到保证?
LAST EDIT @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;
}
【问题讨论】:
-
我不敢相信 C++ 标签中有很好的问题。 +1。
-
可能是Union element alignment的副本,但我不确定
-
@StephenLin:我必须承认我觉得这段代码很可疑;我怀疑标准中是否有任何保证不能在基础对象和派生对象的第一个属性之间插入填充(即使那会很愚蠢......)
-
@StephenLin:我怀疑你也可以用不同的数组来做到这一点。再次因为可能的填充问题。例如,想象一下通过在每个数组周围留下“红色区域”来检测越界访问来检测构建。并且标准非常清楚,理论上,两个不同的对象意味着未定义的行为(我相信其余的近/远指针及其不同的地址空间)。
-
零长度数组无效。
标签: c++ pointers c++11 language-lawyer pointer-arithmetic