【问题标题】:Why (if that is the case) does the standard say that copying uninitialized memory with memcpy is UB?为什么(如果是这样的话)标准说用 memcpy 复制未初始化的内存是 UB?
【发布时间】:2021-06-17 12:38:00
【问题描述】:

当一个类成员在构造的那一刻不能有有意义的意义时, 我不初始化它。显然,这只适用于 POD 类型,你不能 NOT 用构造函数初始化一个对象。

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

例如,

struct MathProblem {
  bool finished;
  double answer;

  MathProblem() : finished(false) { }
};

在数学问题解决(完成)之前,没有答案。提前初始化answer(到零)是没有意义的,因为这可能不是答案。 answer 仅在 finished 设置为 true 后才有意义。

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

但是,answer 在初始化之前的一个简单副本当前也是 UB(如果我正确理解标准的话),这没有意义:默认的复制和移动构造函数应该能够简单地创建一个微不足道的副本(又名,好像使用 memcpy),是否已初始化:我可能想将此对象移动到容器中:

v.push_back(MathProblem());

然后使用容器内的副本。

移动一个具有未初始化的、可简单复制的成员的对象是否确实被标准定义为 UB?如果是这样,为什么?这似乎没有意义。

【问题讨论】:

  • 在 C 中,复制这种未初始化的 POD 可能会导致“不确定的值:未指定的值或陷阱表示”。它陷阱表示刺痛。
  • @chux-ReinstateMonica int 在表示陷阱表示时有什么值?
  • bool finished; int answer; 那是std::optional 就在这里。
  • int 有陷阱值时,它没有数值。 “陷阱表示:不需要表示对象类型的值的对象表示。”如今,int 的陷阱值很少见。今天更常见的是指针,但总体上并不常见。
  • @DavisHerring:无条件复制answer 的复制构造函数可能比在finished 为假时竭尽全力避免复制它的构造函数更快。

标签: c++ language-lawyer standards undefined-behavior trivially-copyable


【解决方案1】:

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

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

[basic.indet]

当一个对象获得自动或动态存储时长的存储时,该对象有一个不确定的值,如果没有对该对象执行初始化,该对象将保留一个不确定的值,直到该值被替换([expr.ass ])。

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

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

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

    那么运算的结果就是一个不确定的值。

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

  • 如果在初始化无符号普通字符类型的对象时,初始化表达式的评估产生了无符号普通字符类型的不确定值,则该对象被初始化为不确定值。 如果在初始化 std​::​byte 类型的对象时,通过初始化表达式的评估产生了无符号普通字符类型或 std​::​byte 类型的不确定值,则该对象被初始化为不确定值。

没有任何例外情况适用于您的示例对象,因此 UB 适用。


with memcpy 是 UB 吗?

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


为什么?

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


我可能想将此对象移动到容器中:

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

【讨论】:

  • 它不是一个未初始化的对象,它是一个具有尚未初始化的普通可复制成员(又名,具有不确定值)并且(因此)保证不被使用的对象;我会说“阅读”——但我必须定义复制不是阅读。恕我直言,不是。即使陷阱值也应该可以复制到相同类型的恕我直言而不会触发陷阱:复制数据不是使用它。对象本身将具有已初始化的其他成员(否则将永远无法知道何时可以读取不确定的成员)。
  • @CarloWood 使不确定值 UB 的规则不是说“使用该值”,而是说“如果一个不确定的值是由评估产生的”。
  • Doing:double x, y; y = x; 几乎不是评估,是吗?
  • @CarloWood:在什么意义上声明y=x; 不会评估x
  • @supercat 它会复制一个可简单复制的对象。它应该与std::memcpy(&y, &x, sizeof(double)); 相同——它是等价的,所以即使 x 具有不确定值(y = x 即 memcpy我不明白吗)。
【解决方案2】:

C++ 标准的设计深受 C 标准的影响,其作者(根据已发布的基本原理)打算并期望在实现质量的基础上,通过有意义地扩展语言的语义在很明显这样做有用的情况下处理程序,即使标准没有“正式”定义这些程序的行为。因此,这两个标准都更重视确保它们不强制要求行为,因为这样做可能会使某些实施变得不那么有用,而不是确保它们强制要求质量通用实施应支持的所有内容。

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

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

【讨论】:

    猜你喜欢
    • 2017-02-16
    • 1970-01-01
    • 2022-09-27
    • 2017-10-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多