子对象边界上的指针算法

Ste*_*Lin 24 c++ pointers pointer-arithmetic language-lawyer c++11

以下代码(跨子对象边界执行指针运算)是否具有明确定义的行为,适用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;
}
Run Code Online (Sandbox Code Playgroud)

LLVM在内部向量类型的实现中使用上述技术的变体,该内部向量类型被优化以最初将堆栈用于小型阵列,但是在初始容量上切换到堆分配的缓冲区.(之所以做这种方式是不是从这个例子清楚,但显然是要减少模板代码膨胀,这是更清楚,如果你去翻代码.)

注意:在任何人抱怨之前,这并不是他们正在做的事情,可能是他们的方法比我在这里给出的标准更符合标准,但我想询问一般情况.

显然,它在实践中有效,但我很好奇,如果标准中的任何内容保证是这样的话.考虑到N3242/expr.add,我倾向于拒绝.

当减去指向同一数组对象的元素的两个指针时,结果是两个数组元素的下标的差异...此外,如果表达式P指向数组对象的元素或者指向最后一个元素的元素对于数组对象,并且表达式Q指向同一数组对象的最后一个元素,表达式((Q)+1) - (P)具有与((Q) - (P))+ 1相同的值,并且as - ((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;
}
Run Code Online (Sandbox Code Playgroud)

结合有关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;
}
Run Code Online (Sandbox Code Playgroud)

此外,如果==不够强大,如果我们利用std::less形成指针总数的事实,并将上面的条件更改为:

    if((aligned = (!std::less<float *>()(p, q) && !std::less<float *>()(q, p))))
Run Code Online (Sandbox Code Playgroud)

代码是否假设两个相等的指针指向同一个数组对象,根据严格的标准读取确实破坏了?

编辑抱歉,只想添加一个示例,以消除标准布局问题:

#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;
}
Run Code Online (Sandbox Code Playgroud)

Arn*_*rtz 8

更新:这个答案起初错过了一些信息,从而得出了错误的结论.

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

  • UB,如果你使用指针的差异.(§5.7,6)
  • 未指定,如果使用关系运算符(第5.2,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;
}
Run Code Online (Sandbox Code Playgroud)

标记的行(*)很有趣: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;
}
Run Code Online (Sandbox Code Playgroud)

标有该生产线(!)不是 UB,但其结果是实现定义的,因为填充,对齐和提到instumentation可能会起到一定的作用.但是,如果该断言成立,您可以使用两个对象部分,如一个数组.

你会知道在记忆中rest[0]会立即产生initial[0].乍一看,你不能轻易使用平等:

  • initial[1]会指出一个接一个的结束initial,取消引用它是UB.
  • rest[-1] 显然是出界的.

但是进入§3.9.2,3:

如果类型的对象T位于地址A,则表示值为地址的类型为cv T*的指针A将指向该对象,而不管该值是如何获得的.[注意:例如,超过数组末尾的地址(5.7)将被视为指向可能位于该地址的数组元素类型的无关对象.

因此&initial[1] == &rest[0],如果只有一个数组,它将是二进制的,并且一切都会好的.

您可以迭代两个数组,因为您可以在边界处应用一些"指针上下文切换".所以到你的最后一个片段:swap不需要!

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

如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出; 否则,行为未定义.

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

  • "好路径":没问题&initial[1],因为&initial[1] == &rest[0]你可以接受该地址并继续增加指针以访问其他元素rest,因为§3.9.2,3
  • "坏路径":initial[2]*(initial + 2),但是因为§5.7,5,initial +2已经是UB而你永远不会在这里使用§3.9.2,3.

在一起:你必须在边界处停留,短暂休息以检查地址是否相等然后你可以继续前进.