添加到“char *”指针UB,当它实际上没有指向char数组?

时间:2017-11-26 16:47:39

标签: c++ language-lawyer

C ++ 17(expr.add/4)说:

  

添加或减去具有整数类型的表达式时   从指针开始,结果具有指针操作数的类型。如果   表达式P指向具有n的数组对象x的元素x [i]   元素,表达式P + J和J + P(其中J的值为j)   如果0≤i+j≤n,则指向(可能是假设的)元素x [i + j];   否则,行为未定义。同样,表达式P-J   如果0≤i-j≤n,则指向(可能是假设的)元素x [i-j];   否则,行为未定义。

struct Foo {
    float x, y, z;
};

Foo f;
char *p = reinterpret_cast<char*>(&f) + offsetof(Foo, z); // (*)
*reinterpret_cast<float*>(p) = 42.0f;

该行标有(*)UB吗? reinterpret_cast<char*>(&f)不指向char数组,而是指向float,因此根据引用的段落它应该是UB。但是,如果它是UB,那么offsetof的用处将是有限的。

是UB吗?如果没有,为什么不呢?

5 个答案:

答案 0 :(得分:6)

此添加旨在有效,但我不相信该标准设法说得那么清楚。引用N4140(大致是C ++ 14):

  

3.9类型[basic.types]

     

2对于普通可复制类型T的任何对象(基类子对象除外),无论对象是否包含类型T的有效值,基础字节(1.7)都会生成向上对象可以复制到数组中   charunsigned char 42 [...]

     

42)例如,使用库函数(17.6.1.2)std::memcpystd::memmove

它说&#34;例如&#34;因为std::memcpystd::memmove不是允许复制基础字节的唯一方法。手动复制的简单for循环也应该是有效的。

为了使其工作,必须为构成对象的原始字节的指针定义加法,并且表达式的定义方式有效,加法的定义不能取决于是否添加&#39 ; s结果随后将用于将字节复制到数组中。

这是否意味着那些字节已经形成一个数组,或者这是否是+运算符的一般规则的特殊例外,在运算符描述中以某种方式被省略,我不清楚(我怀疑前者) ),但无论哪种方式都会使您在代码中执行的添加有效。

答案 1 :(得分:3)

任何不允许offsetof使用的解释都必须是错误的:

#include <assert.h>
#include <stddef.h>
struct S { float a, b, c; };

const size_t idx_S[] = {
    offsetof(struct S, a),
    offsetof(struct S, b),
    offsetof(struct S, c),
};

float read_S(struct S *sp, unsigned int idx)
{
    assert(idx < 3);
    return *(float *)(((char *)sp) + idx_S[idx]); // intended to be valid
}

但是,任何允许人们超越显式声明的数组结尾的解释也必须是错误的:

#include <assert.h>
#include <stddef.h>
struct S { float a[2]; float b[2]; };

static_assert(offsetof(struct S, b) == sizeof(float)*2,
    "padding between S.a and S.b -- should be impossible");

float read_S(struct S *sp, unsigned int idx)
{
    assert(idx < 4);
    return sp->a[idx]; // undefined behavior if idx >= 2,
                       // reading past end of array
}

我们现在处于两难境地,因为C和C ++标准中的措辞,旨在禁止第二种情况,可能也不允许第一种情况。

这通常被称为“什么是对象?”问题。自20世纪90年代以来,人们,包括C和C ++委员会的成员,一直在争论这个问题和相关问题,并且已经多次尝试修复措辞,据我所知,没有人成功(在所有意义上现有的“合理”代码绝对符合要求,并且仍允许所有现有的“合理”优化。

(注意:以上所有代码都是用C语言编写的,以强调两种语言都存在相同的问题,并且可以在不使用任何C ++构造的情况下遇到。)

答案 2 :(得分:1)

据我所知,您的代码有效。根据§3.10¶10.8明确允许将对象别名为char数组:

  

如果程序试图通过以下类型之一以外的glvalue访问对象的存储值,则行为未定义:

     
      
  • [...]
  •   
  • charunsigned char类型。
  •   

另一个问题是,是否将char*指针强制转换回float*并通过它指定是有效的。由于您的Foo是POD类型,因此可以。您可以计算POD成员的地址(假设计算本身不是UB),然后通过该地址访问该成员。例如,您不得滥用此权限来访问非POD对象的private成员。此外,如果您倾向于转换为int*或写入不存在float类型的对象的地址,那么它将是UB。这背后的原因可以在上面引用的部分找到。

答案 3 :(得分:1)

是的,这是不确定的。如您在问题中所述,

  

reinterpret_cast<char*>(&f)不是指向char数组,而是指向浮点数,...

... reinterpret_cast<char*>(&f) does even not point to a char,因此即使对象表示形式为char数组,其行为仍未定义。

对于offsetof,您仍然可以像

一样使用它
struct Foo {
    float x, y, z;
};

Foo f;
auto p = reinterpret_cast<std::uintptr_t>(&f) + offsetof(Foo, z); 
                       // ^^^^^^^^^^^^^^
*reinterpret_cast<float*>(p) = 42.0f;

答案 4 :(得分:1)

请参见CWG 1314

  

根据6.9 [basic.types]第4段,

     
    

类型T的对象的对象表示形式是由类型T的对象占用的N个无符号字符对象的序列,其中N等于sizeof(T)。

  
     

和4.5 [介绍对象]第5段,

     
    

普通可复制或标准布局类型(6.9 [basic.types])的对象应占据连续的存储字节。

  
     

这些段落是否使标准布局对象中的指针算术运算(8.7 [expr.add]第5段)得到了明确定义(例如,用于编写自己的memcpy版本)?

     

理论值(2011年8月):

     

当前的措辞足够清楚,表明可以使用这种用法。

我强烈不同意CWG的说法,即“当前措词已经足够清楚”,但这是我们的裁定。

我将CWG的响应解释为,建议将指向unsigned char的指针转换为普通可复制或标准布局类型的对象,以进行指针算术,应将其解释为指向{{ 1}}的大小等于所讨论对象的大小。我不知道他们是否打算使用unsigned char指针或char指针(从C ++ 17开始)也可以使用它。 (也许如果他们决定进行实际澄清,而不是声称现有措辞足够清楚,那么我会知道答案的。)

(一个单独的问题是是否需要std::byte才能使OP的代码定义正确。我在这里不做介绍;我认为这值得一个单独的问题。)