今天我遇到了一些大致类似于以下代码段的代码。 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类型读取填充字节并将它们与其他东西进行比较是不确定的行为?我在标准和StackOverflow中都找不到确定的答案。
答案 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} },char
和unsigned 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
到另一个这样的结构。