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,我倾向于说不:

当两个指向同一个数组对象的元素的指针相减时,结果是两个数组元素的下标之差……此外,如果表达式 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;
}
4

1 回答 1

9

更新:这个答案起初错过了一些信息,因此导致错误的结论。

在您的示例中,initial并且rest显然是不同的(数组)对象,因此将指向initial(或其元素)的指针与指向rest(或其元素)的指针进行比较是

  • UB,如果您使用指针的差异。(§5.7,6)
  • 未指定,如果您使用关系运算符(§5.9,2)
  • 定义明确==(所以第二次剪断很好,见下文)

第一个片段:

对于您提供的报价(§5.7,6),在第一个片段中建立差异是未定义的行为:

除非两个指针都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,否则行为是未定义的。

澄清第一个示例代码的 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.begin并且d.end不是同一数组的元素,因此操作结果为 UB。尽管事实上您可能reinterpret_cast<char*>(&d)在结果数组中同时拥有它们的地址,但还是会这样做。但是由于该数组是所有的表示,d因此不能将其视为. d因此,尽管该操作可能会起作用并在任何人可以梦想的实现中给出预期的结果,但它仍然是 UB - 作为定义问题。

第二个片段:

这实际上是定义明确的行为,但实现定义的结果:

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 ,但其结果是implementation defined,因为填充、对齐和提到的工具可能会起作用。但是,如果该断言成立,您可以像使用一个数组一样使用两个对象部分

你会知道那rest[0]会立即initial[0]留在记忆中。乍一看,你不能轻易使用等式:

  • initial[1]将指向 的末尾initial,取消引用它是 UB。
  • rest[-1]显然是越界了。

但进入§3.9.2,3

如果一个类型的对象T位于一个地址,那么一个值为地址的cvA类型的指针被称为指向该对象,而不管该值是如何获得的。[注意:例如,数组末尾的地址(5.7)将被认为指向可能位于该地址的数组元素类型的不相关对象。 T*A

因此&initial[1] == &rest[0],如果它是二进制的,就好像只有一个数组一样,一切都会好的。

您可以遍历两个数组,因为您可以在边界处应用一些“指针上下文切换”。所以对于你的最后一个片段:swap不需要!

但是,有一些警告:rest[-1]是 UB,因此也是initial[2],因为§5.7,5

如果指针操作数和结果都指向同一个数组对象的元素,或者超过数组对象的最后一个元素,则计算不应产生溢出;否则,行为是 undefined

(强调我的)。那么这两者是如何结合在一起的呢?

  • “好路径”:没问题&initial[1],因为&initial[1] == &rest[0]您可以获取该地址并继续增加指针以访问 的其他元素rest,因为 §3.9.2,3
  • "Bad path": initial[2]is *(initial + 2),但由于 §5.7,5initial +2已经是 UB,您永远无法在此处使用 §3.9.2,3。

一起:您必须在边界处停下来,稍作休息以检查地址是否相等,然后您可以继续前进。

于 2013-03-05T10:30:42.490 回答