复制具有未初始化成员的结构

Posted

技术标签:

【中文标题】复制具有未初始化成员的结构【英文标题】:Copying structs with uninitialized members 【发布时间】:2020-05-23 13:18:17 【问题描述】:

复制其中一些成员未初始化的结构是否有效?

我怀疑这是未定义的行为,但如果是这样,它会使任何未初始化的成员留在结构中(即使这些成员从未直接使用过)非常危险。所以我想知道标准中是否有允许它的东西。

例如,这有效吗?

struct Data 
  int a, b;
;

int main() 
  Data data;
  data.a = 5;
  Data data2 = data;

【问题讨论】:

我记得不久前看到过类似的问题,但找不到。此question 与this one 相关。 【参考方案1】:

是的,如果未初始化的成员不是无符号窄字符类型或std::byte,则使用隐式定义的复制构造函数复制包含此不确定值的结构在技术上是未定义的行为,因为它用于复制具有不确定值的变量同类型,因为[dcl.init]/12。

这适用于此,因为隐式生成的复制构造函数(unions 除外)被定义为单独复制每个成员,就像通过直接初始化一样,请参阅[class.copy.ctor]/4。

这也是当前CWG issue 2264 的主题。

不过,我想在实践中你不会有任何问题。

如果您想 100% 确定,如果类型为 trivially copyable,则使用 std::memcpy 始终具有明确定义的行为,即使成员具有不确定的值。


抛开这些问题不谈,无论如何,您应该始终在构造时使用指定的值正确初始化您的类成员,假设您不需要类具有trivial default constructor。您可以使用默认的成员初始化器语法轻松地做到这一点,例如值初始化成员:

struct Data 
  int a, b;
;

int main() 
  Data data;
  data.a = 5;
  Data data2 = data;

【讨论】:

好吧..该结构不是 POD(普通旧数据)?这意味着成员将使用默认值初始化?这是一个疑问 这种情况下不就是浅拷贝吗?除非在复制的结构中访问未初始化的成员,否则会出现什么问题? @KevinKouketsu 我为需要琐碎/POD类型的情况添加了一个条件。 @TruthSeeker 标准说这是未定义的行为。 AndreySemashev 在回答中解释了(非成员)变量通常是未定义行为的原因。基本上它是支持具有未初始化内存的陷阱表示。这是否打算应用于结构的隐式复制构造是链接的 CWG 问题的问题。 @TruthSeeker 隐式复制构造函数被定义为单独复制每个成员,就像通过直接初始化一样。它没有被定义为像memcpy 那样复制对象表示,即使对于普通的可复制类型也是如此。唯一的例外是联合,它的隐式复制构造函数确实复制了对象表示,就像 memcpy 一样。【参考方案2】:

通常,复制未初始化的数据是未定义的行为,因为该数据可能处于捕获状态。引用this页面:

如果对象表示不表示对象类型的任何值,则称为陷阱表示。以任何方式访问陷阱表示,而不是通过字符类型的左值表达式读取它是未定义的行为。

浮点类型可以发送信号 NaN,在某些平台上整数 may have 陷阱表示。

但是,对于trivially copyable 类型,可以使用memcpy 来复制对象的原始表示。这样做是安全的,因为不会解释对象的值,而是复制对象表示的原始字节序列。

【讨论】:

评论不用于扩展讨论;这个对话是moved to chat。【参考方案3】:

在某些情况下,例如所描述的情况,C++ 标准允许编译器以客户认为最有用的任何方式处理构造,而不要求行为是可预测的。换言之,此类构造调用“未定义行为”。然而,这并不意味着这样的结构是“禁止的”,因为 C++ 标准明确放弃了对“允许”格式良好的程序做什么的管辖权。虽然我不知道任何已发布的 C++ 标准基本原理文档,但它描述未定义行为的事实与 C89 非常相似,这表明其预期含义是相似的:“未定义行为使实现者许可不捕获某些困难的程序错误进行诊断。它还确定了可能的符合语言扩展的区域:实现者可以通过提供官方未定义行为的定义来扩充语言。

在很多情况下,最有效的处理方式是编写结构中下游代码会关心的部分,同时省略下游代码不关心的部分。要求程序初始化结构的所有成员,包括那些永远不会关心的成员,会不必要地影响效率。

此外,在某些情况下,让未初始化的数据以不确定的方式表现可能是最有效的。例如,给定:

struct q  unsigned char dat[256];  x,y;

void test(unsigned char *arr, int n)

  q temp;
  for (int i=0; i<n; i++)
    temp.dat[arr[i]] = i;
  x=temp;
  y=temp;

如果下游代码不关心x.daty.dat 的索引未在arr 中列出的任何元素的值,则代码可能会优化为:

void test(unsigned char *arr, int n)

  q temp;
  for (int i=0; i<n; i++)
  
    int it = arr[i];
    x.dat[index] = i;
    y.dat[index] = i;
  

如果要求程序员在复制 temp.dat 的每个元素(包括下游不关心的那些元素)之前显式编写它,那么效率的提高是不可能的。

另一方面,在某些应用程序中,避免数据泄露的可能性很重要。在这样的应用程序中,有一个版本的代码可能很有用,该版本被检测为捕获任何复制未初始化存储的尝试,而不考虑下游代码是否会查看它,或者有一个实现保证任何存储可能很有用其内容可能被泄露的内容将被归零或被非机密数据覆盖。

据我所知,C++ 标准并没有试图说这些行为中的任何一个都比另一个更有用,以证明强制它是合理的。具有讽刺意味的是,这种缺乏规范可能旨在促进优化,但如果程序员不能利用任何类型的弱行为保证,任何优化都将被否定。

【讨论】:

恕我直言,有些人对 UB 过于敏感。你的回答很有道理。 @InnocentBystander:在 2005 年左右,忽略标准编译器 可以 做什么和通用编译器 应该 做什么之间的区别变得很流行,并且还优先考虑实现可以处理“完全可移植”程序的效率,而不是它可以最有效地完成手头任务的效率(这可能需要使用“不可移植”的构造但得到广泛支持)。【参考方案4】:

由于Data 的所有成员都是原始类型,data2 将获得data 的所有成员的精确“逐位副本”。所以data2.b 的值将与data.b 的值完全相同。但是,无法预测 data.b 的确切值,因为您尚未明确初始化它。这将取决于分配给data 的内存区域中的字节值。

【讨论】:

您引用的片段谈到了 memmove 的行为,但在这里并不真正相关,因为在我的代码中我使用的是复制构造函数,而不是 memmove。其他答案暗示使用复制构造函数会导致未定义的行为。我认为您也误解了“未定义的行为”一词。这意味着该语言根本不提供任何保证,例如该程序可能会随机崩溃或损坏数据或执行任何操作。这不仅仅意味着某些值是不可预测的,这将是未指定的行为。 @TomekCzajka:当然,根据标准的作者,UB“......确定了可能的符合语言扩展的区域:实现者可以通过提供官方未定义行为的定义来增强语言。”有一个疯狂的神话说标准的作者为此目的使用了“实现定义的行为”,但这样的概念与他们实际编写的内容完全矛盾。 @TomekCzajka:在由早期标准定义的行为在后来的标准中变得未定义的情况下,委员会的意图通常不是弃用旧行为,而是说 如果一个实施可以通过做其他事情来最好地服务于它的客户,委员会不想禁止他们这样做。与该标准的一个主要混淆点源于委员会成员对其预期管辖权缺乏共识。大多数程序要求仅适用于严格符合程序... @TomekCzajka:我认为如果标准能够识别通过有效指针访问其存储值的对象必须表现得好像使用定义的表示存储,但存储的值是不能通过指针访问的可能使用其他可能具有陷阱值的表示,即使定义的表示没有。这将允许例如一个具有两个 uint16_t 值的自动持续时间结构可能会使用两个 32 位寄存器来存储,这些寄存器的值不会被初始化,并且可能表现得很奇怪...... @InnocentBystander:短语“陷阱表示”不仅仅指访问时触发 CPU 陷阱的事物,还适用于其表示可能违反编译器预期不变量的对象,其后果可能是比操作系统陷阱更糟糕。例如,给定uint1 = ushort1; ... if (uint1 &lt; 70000) foo[uint1] = 123;,编译器可能生成的代码总是使uint1 在该路径上小于70000,它可能生成代码,其中uint1 的值可能大于69999,但执行比较并跳过分配如果是,或者它可能......

以上是关于复制具有未初始化成员的结构的主要内容,如果未能解决你的问题,请参考以下文章

C++ 未初始化的结构成员 - 字段不存在

如何初始化(或分配)具有特定默认值的结构(抽象数据类型)成员的值

编译器未捕获未初始化的成员。它是一个错误吗?

valgrind 抱怨 C++ 结构上的未初始化字节

c语言怎么结构数据初始化?

C++ Struct 未通过 POD 测试