复制未初始化成员的结构体

32

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

我怀疑这是未定义行为,但如果是这样的话,即使那些成员从未被直接使用,留下任何未初始化的结构体成员也会非常危险。因此,我想知道标准中是否有允许这样做的内容。

例如,下面的代码是否有效?

struct Data {
  int a, b;
};

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

我记得以前看到过类似的问题,但找不到了。这个 问题这个问题有关联。 - 1201ProgramAlarm
4个回答

25

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

这也适用于这里,因为除了union之外,隐式生成的复制构造函数被定义为按照直接初始化的方式单独复制每个成员,参见[class.copy.ctor]/4

这也是活跃的CWG问题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(普通旧数据)吗?这意味着成员将被初始化为默认值?这是一个疑问。 - Kevin Kouketsu
在这种情况下,这不是浅拷贝吗?除非在复制的结构体中访问了未初始化的成员,否则有什么问题呢? - TruthSeeker
@KevinKouketsu 我已经添加了一个条件,用于需要平凡/POD类型的情况。 - walnut
@TruthSeeker 标准规定这是未定义行为。AndreySemashev的回答解释了为什么对于(非成员)变量通常是未定义行为。基本上,这是为了支持使用未初始化内存的陷阱表示。这是否意图适用于结构体的隐式复制构造是链接CWG问题的问题。 - walnut
@TruthSeeker 隐式复制构造函数被定义为逐个成员复制,就像通过直接初始化一样。它不是按照memcpy的方式复制对象表示,即使对于可平凡复制的类型也是如此。唯一的例外是联合体,隐式复制构造函数确实会按照memcpy的方式复制对象表示。 - walnut
显示剩余6条评论

13
一般而言,复制未初始化的数据是未定义行为,因为该数据可能处于陷阱状态。引用页面的说法:

如果对象表示不代表任何对象类型的值,则称其为Trap Representation。以任何方式访问Trap Representation(除了通过字符类型的Lvalue表达式读取它)都是未定义行为。

浮点类型可能有Signalling NaNs,一些平台上整数可能具有Trap Representation。
但是,对于“trivially copyable”类型,可以使用memcpy复制对象的原始表示。这样做是安全的,因为对象的值不被解释,而是复制对象表示的原始字节序列。

评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew

0
在某些情况下,如所述的情况,C++标准允许编译器以客户最有用的方式处理结构,而不要求行为可预测。换句话说,这些构造会调用“未定义行为”。然而,这并不意味着这些构造是“禁止”的,因为C ++标准明确放弃了对“允许”做什么的规定权。虽然我不知道C ++标准的任何已发布理论文档,但它描述Undefined Behavior的事实与C89类似,这意味着预期的含义是相似的:“Undefined behavior使实现者获得不捕获某些难以诊断的程序错误的许可。它还确定可能符合的语言扩展领域:实现者可以通过提供正式未定义行为的定义来增加语言。”。
有很多情况需要以最有效的方式处理一些东西,这需要编写下游代码关心的结构的部分,同时省略下游代码不关心的结构。要求程序初始化结构的所有成员,包括永远不关心的成员,将无端地妨碍效率。
此外,有些情况下,将未初始化的数据行为设置为非确定性可能是最有效的。例如,给定:
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;
}

如果下游代码不关心未在arr中列出的x.daty.dat的任何元素的值,则可以对代码进行优化:
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++标准并没有试图说这些行为中的任何一个足够有用,以至于要强制执行它。具有讽刺意味的是,这种缺乏规范可能旨在促进优化,但如果程序员不能利用任何类型的弱行为保证,任何优化都将被抵消。


在我看来,有些人对未定义行为过于敏感。你的回答很有道理。 - Super-intelligent Shade
1
@InnocentBystander:大约在2005年左右,忽略符合编译器可以做什么和通用编译器应该做什么之间的区别变得时尚,并且优先考虑实现能够处理“完全可移植”程序的效率,而不是它能够以最高效的方式完成手头任务的效率(这可能涉及使用“非可移植”但广泛支持的结构)。 - supercat

-2

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


1
你引用的片段谈到了memmove的行为,但在我的代码中我使用的是复制构造函数,所以这并不相关。其他答案暗示使用复制构造函数会导致未定义的行为。我认为你也误解了“未定义的行为”这个术语。它意味着语言根本没有提供任何保证,例如程序可能会随机崩溃或破坏数据,或者做任何事情。它并不仅仅意味着某些值是不可预测的,那将是未指定的行为。 - Tomek Czajka
1
@TomekCzajka:当然,根据标准的作者们所说,UB“...确定了可能符合语言扩展的领域:实现者可以通过提供官方未定义行为的定义来增强语言。”有一个疯狂的神话认为标准的作者们用“实现定义行为”来达到这个目的,但这种想法与他们实际写下的内容完全相矛盾。 - supercat
1
@TomekCzajka:在早期标准定义的行为在后来的标准中变得未定义的情况下,委员会的意图通常不是废弃旧行为,而是说如果实现可以通过做其他事情来最好地服务其客户,委员会不希望禁止他们这样做。标准的一个主要混淆点源于委员会成员之间对其拟定管辖范围的意见分歧。大多数程序要求仅适用于严格符合规范的程序... - supercat
1
@InnocentBystander:“陷阱表示法”这个词汇不仅指触发CPU陷阱的事物,还适用于其表示可能违反编译器预期的不变量的对象,其后果可能比操作系统陷阱更严重。例如,给定uint1 = ushort1; ... if (uint1 < 70000) foo[uint1] = 123;,编译器可能会生成代码,在该路径上始终使uint1小于70000,它可能会生成代码,其中uint1可能会保存一个大于69999的值,但如果是,则执行比较并跳过赋值,或者它可能... - supercat
1
生成代码时,uint1 可能会保存一个大于 70000 的值,但如果没有定义 uint1 可以这么大的方式,就无条件执行赋值操作。不幸的是,标准术语无法让编译器在程序员不关心 uint1 将保存什么值的情况下选择前两种方法中最有效的一种,但不允许编译器同时生成可能将 uint1 设置为大于 65535 的值的代码,并假设 uint1 永远不会接收到这样的值。 - supercat
显示剩余17条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接