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

Copying structs with uninitialized members

提问人:Tomek Czajka 提问时间:2/7/2020 最后编辑:L. F.Tomek Czajka 更新时间:2/16/2020 访问量:2278

问:

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

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

例如,这有效吗?

struct Data {
  int a, b;
};

int main() {
  Data data;
  data.a = 5;
  Data data2 = data;
}
C++ 初始化 复制构造函数 undefined-behavior

评论

0赞 1201ProgramAlarm 2/7/2020
我记得前段时间看到过类似的问题,但找不到。这个问题这个问题是相关的。

答:

13赞 Andrey Semashev 2/7/2020 #1

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

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

对于浮点类型,可以信令 NaN,在某些平台上,整数可能具有陷阱表示形式。

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

评论

0赞 Samuel Liew 6/30/2020
评论不用于扩展讨论;此对话已移至 Chat
25赞 walnut 2/7/2020 #2

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

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

这也是正在进行的 CWG 第 2264 期的主题。

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

如果要 100% 确定,如果类型是可复制的,则即使成员具有不确定的值,则 using 始终具有明确定义的行为。std::memcpy


撇开这些问题不谈,无论如何,您都应该始终在构造时使用指定的值正确地初始化您的类成员,假设您不需要类具有简单的默认构造函数。您可以使用默认成员初始值设定项语法轻松做到这一点,例如对成员进行值初始化:

struct Data {
  int a{}, b{};
};

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

评论

0赞 Kevin Kouketsu 2/7/2020
井。。该结构不是 POD(普通旧数据)?这意味着成员将使用默认值进行初始化?这是一个疑问
0赞 TruthSeeker 2/7/2020
在这种情况下,这不是浅拷贝吗?除非在复制的结构中访问未初始化的成员,否则这会出什么问题?
0赞 walnut 2/7/2020
@KevinKouketsu 我为需要琐碎/POD 类型的情况添加了一个条件。
0赞 walnut 2/7/2020
@TruthSeeker 标准说这是未定义的行为。AndreySemashev 在回答中解释了(非成员)变量通常是未定义行为的原因。基本上,它是为了支持具有未初始化内存的陷阱表示。这是否适用于结构的隐式副本构造是相关的CWG问题。
0赞 walnut 2/7/2020
@TruthSeeker 隐式复制构造函数被定义为单独复制每个成员,就像通过直接初始化一样。它没有定义为复制对象表示,就好像 by 一样,即使对于简单的可复制类型也是如此。唯一的例外是联合,对于联合,隐式复制构造函数会像复制 一样复制对象表示形式。memcpymemcpy
0赞 supercat 2/10/2020 #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.datarr

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++标准并没有试图说这些行为中的任何一个都比另一个行为更有用,以至于有理由强制执行它。具有讽刺意味的是,这种规范的缺乏可能是为了促进优化,但如果程序员不能利用任何一种弱行为保证,任何优化都会被否定。

评论

0赞 Super-intelligent Shade 12/12/2021
恕我直言,有些人对UB太敏感了。你的回答是有道理的。
1赞 supercat 12/12/2021
@InnocentBystander:大约在2005年,人们开始流行一种趋势,即忽略符合要求的编译器可以做什么,而通用编译器应该做什么,并且优先考虑实现处理“完全可移植”程序的效率,而不是最有效地完成手头任务的效率(这可能需要使用“不可移植”但得到广泛支持的构造)。
-2赞 ivan.ukr 2/12/2020 #4

由于 的所有成员都是原始类型,因此将获得 的所有成员的精确“逐位复制”。因此,的值将与 的值完全相同。但是,无法预测 的确切值,因为您尚未显式初始化它。这将取决于内存区域中分配给 的字节值。Datadata2datadata2.bdata.bdata.bdata

评论

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