12

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

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 类型读取填充字节并将它们与其他内容进行比较是未定义的行为吗?我在 Standard 和 StackOverflow 上都找不到明确的答案。

4

3 回答 3

9

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


1) 在 C++14 之前:由于您的 1 的补码或有符号幅度signed类型的可能性,由于比较 +0 和 -0 char,您可能会返回令人惊讶的结果。

真正防水的方法是使用const unsigned char*指针。这消除了对现已废除的(来自 C++14)1 的补码或有符号的量级的任何担忧char


由于 (i) 您拥有内存,(ii) 您正在获取指向 的指针x,并且 (iii) anunsigned char不能包含陷阱表示,(iv) charunsigned char并且signed char不受严格的别名规则的约束,因此使用const unsigned char*读取的行为未初始化的内存是完美定义的。


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

于 2017-11-22T14:41:05.823 回答
3

这取决于条件。

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

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

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

0(零)到 T;105

— 如果 T 是(可能是 cv 限定的)非联合类类型,则每个非静态数据成员和每个基类

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

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

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

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

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

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

— 如果 T 是(可能是 cv 限定的)类类型(第 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) 的右操作数的求值产生无符号窄字符类型的不确定值,其第一个操作数是无符号窄字符类型的左值,则不确定值替换引用的对象的值由左操作数。

— 如果在初始化无符号窄字符类型的对象时,初始化表达式的评估产生了无符号窄字符类型的不确定值,则该对象被初始化为不确定值。

于 2017-11-22T17:42:45.067 回答
0

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

坏消息是,我测试过的所有现代编译器(GCC、Clang、MSVC 和 ICC)都忽略了这一点上的标准字母。相反,他们将 C 标准附录 J.2中的粗略声明

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

就好像它在 C 和 C++ 中都是 100% 规范的,即使附件 J 不是规范的。这适用于对未初始化存储的所有可能的读取访问,包括那些通过 小心执行的unsigned char *读取访问,是的,包括对填充字节的读取访问。

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

消息是,如果您检查填充字节的内容,您只会在访问填充字节时产生 UB 。复制它们是可以的。特别是,如果您初始化一个 POD 结构的所有命名字段,通过结构赋值和 复制它是安全的,memcpy使用memcmp.

于 2017-11-23T03:29:55.107 回答