子对象边界上的指针算法

时间:2013-03-05 06:53:30

标签: c++ pointers c++11 language-lawyer pointer-arithmetic

下面的代码(跨子对象边界执行指针运算)是否具有明确定义的类型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指向一个超过数组对象的最后一个元素的值,则其值为零,即使表达式(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;
}

1 个答案:

答案 0 :(得分:8)

更新:这个答案最初错过了一些信息,从而导致错误的结论。

在您的示例中,initialrest明显是不同的(数组)对象,因此将指向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.begind.end不是同一个数组的元素,因此操作会导致UB。尽管您可能reinterpret_cast<char*>(&d)并且在结果数组中同时拥有它们的地址。但由于该数组是{em>所有 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,但其结果是实现定义,因为填充,对齐和提到的设置可能起作用。 但如果该断言将成立,您可以使用两个对象部分,如一个数组

你会知道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。

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