是否未定义行为来读取和比较POD类型的填充字节?

时间:2017-11-22 14:37:46

标签: c++ language-lawyer padding undefined-behavior standard-layout

今天我遇到了一些大致类似于以下代码段的代码。 valgrindUndefinedBehaviorSanitizer都检测到未初始化数据的读取。

template <typename T>
void foo(const T& x)
{
    static_assert(std::is_pod_v<T> && sizeof(T) > 1);
    auto p = reinterpret_cast<const char*>(&x);

    std::size_t i = 1; 
    for(; i < sizeof(T); ++i)
    {
        if(p[i] != p[0]) { break; }
    }

    // ...
}

上述工具抱怨p[i] != p[0]时的比较 包含填充字节的对象传递给foo。例如:

struct obj { char c; int* i; };
foo(obj{'b', nullptr});

从POD类型读取填充字节并将它们与其他东西进行比较是不确定的行为?我在标准和StackOverflow中都找不到确定的答案。

3 个答案:

答案 0 :(得分:9)

您的程序的行为是实现定义有两个方面:

1)在C ++之前14:由于signed可能存在1的补码或带符号的char类型,你可能会返回一个令人惊讶的结果由于比较+0和-0。

真正无懈可击的方法是使用const unsigned char*指针。这消除了现在废除(来自C ++ 14)1的补码或带符号char的任何问题。

由于(i)你拥有记忆,(ii)你正在指向x,(iii)unsigned char不能包含陷阱表示,(iv){{1} },charunsigned char免于严格别名规则,使用signed char读取未初始化内存的行为完全明确定义。

2)但是由于您不知道未初始化的内存中包含的内容,因此读取它的行为未指定,这意味着程序行为是实现定义的,因为char类型不能包含陷阱表示。

答案 1 :(得分:1)

这取决于条件。

如果x为零初始化,则填充为零位,因此这种情况定义良好(C ++ 14的8.5 / 6):

  

零初始化T类型的对象或引用意味着:

     

- 如果T是标量类型(3.9),则将对象初始化为该值   通过转换整数字面积获得

     

0(零)到T; 105

     

- 如果T是(可能是cv-quali fi ed)非联合类类型,则各自   非静态数据成员和每个基类

     

子对象零初始化,填充初始化为零位;

     

- 如果T是(可能是cv-quali fi ed)联合类型,则该对象是第一个   非静态命名数据成员为零 -

     

初始化和填充初始化为零位;

     

- 如果T是数组类型,则每个元素都是零初始化的; - 如果T是   引用类型,不执行初始化。

但是,如果x是默认初始化的,则未指定填充,因此它具有不确定的值(由此处未提及填充的事实推断)(8.5 / 7):

  

默认初始化T类型的对象意味着:

     

- 如果T是(可能是cv-quali fi ed)类类型(第9条),则为默认值   调用T的构造函数(12.1)(初始化为   如果T没有默认构造函数或重载决策,则格式错误   (13.3)导致歧义或被删除的函数或   从初始化的上下文中无法访问);

     

- 如果T是数组类型,则每个元素都是默认初始化的;

     

- 否则,不执行初始化。

比较不确定的值是 UB(对于这种情况),因为没有提到的例外,因为你将不确定的值与某些东西(8.5 / 12)进行比较:

  

如果没有为对象指定初始值设定项,则对象为   默认初始化。使用自动或自动存储对象时   获得动态存储持续时间,该对象具有不确定性   值,如果没有为对象执行初始化,那么   对象保留不确定的值,直到替换该值   (5.17)。 [注意:具有静态或线程存储持续时间的对象是   零初始化,见3.6.2。 - 结束注释] 如果是不确定的值   通过评估产生的行为是不确定的,除了在   以下情况:

     

- 如果无符号窄字符类型的不确定值(3.9.1)   是由以下评估产生的:

     

......-条件表达式的第二或第三个操作数(5.16),

     

......-逗号表达式的右操作数(5.18),

     

......-演员或转换为无符号窄字符类型的操作数(4.7,5.2.3,5.2.9,5.4),

     

     

......-废弃值表达式(第5条),然后是结果   操作是一个不确定的值。

     

- 如果是无符号窄字符类型的不确定值   通过简单赋值的右操作数的评估产生的   运算符(5.17),其第一个操作数是无符号窄值的左值   字符类型,一个不确定的值替换了该值   左操作数引用的对象。

     

- 如果是不确定的值   无符号窄字符类型是由评估产生的   初始化unsigned对象时的初始化表达式   窄字符类型,该对象初始化为不确定   值。

答案 2 :(得分:0)

Bathsheba的回答正确地描述了C ++标准的字母。

坏消息是,我测试过的所有现代编译器(GCC,Clang,MSVC和ICC)都忽略了这一点上的标准字母。相反,他们将Annex J.2中的秃头陈述视为C标准

  

[行为未定义]如果使用具有自动存储持续时间的对象的值是不确定的

尽管附件J不是规范性的,但在C和C ++中,好像它是100%规范的。这适用于对未初始化存储的所有可能的读访问,包括通过unsigned char *精心执行的访问,以及是,包括对填充字节的读访问。

此外,如果您要提交错误报告,我相信您会被告知,如果标准的规范性文本与他们正在做的事情不一致,那就是标准< / em>有缺陷。

好的消息是,如果您检查填充字节的内容,则只有在访问填充字节时才会产生UB。复制它们就可以了。特别是,如果您初始化POD结构的所有命名字段,通过结构分配和memcpy复制它是安全的,但可以安全地比较它使用memcmp到另一个这样的结构。