为什么(如果是这样的话)标准说用memcpy复制未初始化的内存是UB



当类成员在构造时不能具有合理的含义时,我没有初始化它。很明显,这只适用于POD类型,你不能不使用构造函数初始化对象。

这样做的好处除了可以节省CPU周期将某些东西初始化为一个没有意义的值是,我可以检测到这些的错误使用valgrind变量;当我只给出这些变量时,这是不可能的一些随机值。

例如,

struct MathProblem {
bool finished;
double answer;
MathProblem() : finished(false) { }
};

这道数学题解决(做完)之前没有答案。提前初始化answer(比如说为零)是没有意义的,因为这可能不是答案。CCD_ 2只有在CCD_ 3被设置为true之后才具有意义。

因此,在初始化之前使用answer是一个错误,完全可以作为UB。

然而,answer在初始化之前的一个琐碎副本目前也是UB(如果我正确理解标准的话),这是没有意义的:默认的复制和移动构造函数应该能够简单地制作一个琐碎的副本(也就是说,就像使用memcpy一样),无论是否初始化:我可能想把这个对象移动到一个容器中:

v.push_back(MathProblem());

然后使用容器中的副本。

移动一个具有未初始化的、琐碎的可复制成员的对象是否真的被标准定义为UB?如果是,为什么?这似乎没有道理。

移动一个具有未初始化、可复制的成员的对象是否确实被标准定义为UB?

取决于成员的类型。标准说:

〔basic.indet〕

当获得具有自动或动态存储持续时间的对象的存储时,该对象具有不确定的值,并且如果没有对该对象执行初始化,则该对象将保留不确定值,直到该值被替换为止([expr.ass])

如果评估产生了不确定值,行为未定义,以下情况除外:

  • 如果无符号普通字符类型(〔basic.basic〕)或std的不确定值​::​字节类型([cstddef.syn])是通过评估产生的

    • 条件表达式的第二个或第三个操作数
    • 逗号表达式的右操作数
    • 转换或转换([conv.integral]、[expr.type.conv]、[expr.static.cast]、[xpr.cast])为无符号普通字符类型或std的操作数​::​字节类型([cstddef.syn]),或
    • 丢弃的值表达式

    则运算结果为不确定值。

  • 如果是无符号普通字符类型或std的不确定值​::​字节类型是通过计算一个简单赋值运算符([expr.ass])的右操作数产生的,该运算符的第一个操作数是无符号普通字符类型或std的左值​::​字节类型,一个不确定的值将替换左操作数引用的对象的值。

  • 如果在初始化无符号普通字符类型的对象时,通过初始化表达式的求值生成了无符号普通字符串类型的不确定值,则该对象将初始化为不确定值。如果是无符号普通字符类型或std的不确定值​::​字节类型是通过初始化std对象时对初始化表达式的求值产生的​::​字节类型,则该对象被初始化为一个不确定的值。

没有任何异常情况适用于示例对象,因此UB适用。


带memcpy是UB?

不是。std::memcpy将对象解释为字节数组,在这种特殊情况下不存在UB。如果您尝试读取不确定的副本,您仍然有UB(除非上述例外情况适用)。


为什么?

C++标准不包括大多数规则的基本原理。这一特殊规则自第一个标准以来就一直存在。它比有关陷阱表示的C规则稍微严格一些。据我所知,陷阱处理没有既定的约定,作者不希望通过指定它来限制实现,而是选择将它指定为UB。这也具有允许优化器推断不确定值永远不会被读取的效果。


我可能想把这个对象移到一个容器中:

将未初始化的对象移动到容器中通常是一个逻辑错误。目前还不清楚你为什么要做这样的事。

C++标准的设计在很大程度上受到了C标准的影响,C标准的作者(根据已发表的基本原理)打算并期望实现将在实现质量的基础上,通过有意义地处理程序来扩展语言的语义,即使标准没有";正式地";定义这些程序的行为。因此,这两个标准都更优先考虑确保它们不会强制执行行为,因为这样做可能会使某些实现变得不那么有用,而不是确保它们强制执行高质量通用实现应该支持的一切。

在许多情况下,通过保证在任何有效的存储区域上使用memcpy,在最坏的情况下,其行为方式与用一些可能没有意义的比特模式填充目的地一致,而没有外部副作用,来扩展语言的语义可能是有用的,很少有地方让它做其他事情更容易或更有用。任何人都应该关心memcpy的行为是否在涉及有效存储区域的特定情况下定义的唯一情况是,在这些情况下,一些替代行为确实比普通行为更有用。如果存在这种情况,编译器编写者及其客户将比委员会更适合判断哪种行为最有用。

作为一个替代行为可能更有用的情况的示例,请考虑使用memcpy复制部分编写的结构,然后使用它制作该结构的两个副本的代码。在某些情况下,让编译器只写入在原始结构中写入的两个目的地结构的部分可能会提高效率,但这种行为与让第一个memcpy表现得好像它向其目的地存储了一些位模式明显不同。请注意,如果没有以影响行为的方式使用结构中未初始化部分的副本,则这种更改不会对程序的整体行为产生不利影响,但该标准没有很好的方法来区分在这种模块下可能发生或不可能发生的场景,因此未定义所有此类场景。

最新更新