使用未初始化的成员复制结构


29

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

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

例如,这有效吗?

struct Data {
  int a, b;
};

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

我记得不久前看到类似的问题,但找不到。这个问题因为是有关这一个
1201ProgramAlarm

Answers:


23

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

这适用于这里,因为除unions 外,隐式生成的副本构造函数被定义为像通过直接初始化一样分别复制每个成员,请参见[class.copy.ctor] / 4

这也是现行CWG问题2264的主题

我想在实践中您不会对此有任何问题。

如果您想100%确定,std::memcpy则即使类型具有不确定的值,如果类型是可微复制的,使用总是具有明确定义的行为。


除了这些问题,假设您不需要该类具有琐碎的默认构造函数,则无论如何都要在构造时始终使用指定的值正确初始化类成员。您可以使用默认的成员初始化程序语法轻松进行此操作,例如,对成员进行值初始化:

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

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

好..那个结构不是POD(普通旧数据)?这意味着成员将使用默认值初始化吗?令人怀疑
Kevin Kouketsu

在这种情况下不是浅表吗?除非在复制的结构中访问了未初始化的成员,否则这会出错吗?
TruthSeeker

@KevinKouketsu我为需要平凡/ POD类型的情况添加了条件。
胡桃

@TruthSeeker标准说这是未定义的行为。AndreySemashev在答案中解释了它通常是(非成员)变量未定义行为的原因。基本上是用未初始化的内存来支持陷阱表示。这是否打算应用于结构的隐式副本构造是链接的CWG问题的问题。
核桃

@TruthSeeker隐式副本构造函数被定义为单独复制每个成员,就像通过直接初始化一样。没有定义像by一样复制对象表示memcpy,即使对于普通可复制类型也是如此。唯一的例外是联合,其隐式副本构造函数的确复制对象表示形式,就像by一样memcpy
胡桃

11

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

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

对于浮点类型,可能会发信号通知NaN,并且在某些平台上,整数可能具有陷阱表示形式。

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


所有位模式都代表有效值的类型的数据(例如,包含的64字节结构unsigned char[64])如何处理?将结构的字节视为具有未指定的值可能会不必要地阻碍优化,但是要求程序员手动用无用的值填充数组会进一步阻碍效率。
supercat

初始化数据不是没有用的,它可以防止UB,无论它是由陷阱表示还是以后使用未初始化的数据引起的。将64个字节清零(1或2个缓存行)并不像看起来那样昂贵。如果您有昂贵的大型结构,则在复制它们之前应三思而后行。而且我很确定您无论如何都必须初始化它们。
Andrey Semashev

可能不会影响程序行为的机器代码操作是无用的。相对应的想法是,必须不惜一切代价避免任何被标准称为UB的行为,而用[用C标准委员会的话说] UB“确定可能的语言扩展领域”。尽管我还没有看到发布的C ++标准基本原理,但它通过拒绝将程序分类为合格或不合格来明确放弃对“允许”执行C ++程序的管辖权,这意味着它将允许类似的扩展。
超级猫

-1

在某些情况下,例如所描述的那种情况,C ++标准允许编译器以其客户认为最有用的任何方式处理构造,而无需要求行为可预测。换句话说,此类构造调用“未定义行为”。但是,这并不意味着这些构造应被“禁止”,因为C ++标准明确放弃了对“允许”执行的程序的管辖权。虽然我不知道任何已发布的C ++标准的Rationale文档,但它实际上像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.dat或未在y.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 ++标准没有试图说出这些行为中的任何一种都比另一种更为有用,足以证明必须强制执行。具有讽刺意味的是,这种缺乏规范的目的可能是为了促进优化,但是如果程序员不能利用任何类型的弱行为保证,那么任何优化都会被否定。


-2

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


您可以通过参考标准来支持它吗?@walnut提供的链接暗示这是未定义的行为。标准中的POD是否有例外?
Tomek Czajka

尽管以下内容未链接到标准,但仍然:en.cppreference.com/w/cpp/language/… “可以通过手动复制对象表示形式(例如,使用std :: memmove来复制TriviallyCopyable对象。所有与C兼容的数据类型语言(POD类型)可以复制。”
ivan.ukr

在这种情况下,唯一的“不确定行为”是我们无法预测未初始化的成员变量的值。但是代码可以编译并成功运行。
ivan.ukr

1
您引用的片段讨论了memmove的行为,但是在这里并没有实际意义,因为在我的代码中,我使用的是复制构造函数,而不是memmove。其他答案暗示使用复制构造函数会导致未定义的行为。我认为您也误解了“不确定行为”一词。这意味着该语言根本不提供任何保证,例如,程序可能会随机崩溃或破坏数据或执行任何操作。这不仅意味着某些值是不可预测的,而且是未指定的行为。
Tomek Czajka

@ ivan.ukr C ++标准指定隐式copy / move构造函数以成员方式操作,就像通过直接初始化一样,请参阅我的答案中的链接。因此,拷贝构造并没有做出“ ‘逐位复制’ ”。你是唯一的工会类型,其中隐含的拷贝构造函数正确指定为如果由手工作业到对象表示复制std::memcpy。这些都不能阻止使用std::memcpystd::memmove。它仅防止使用隐式副本构造函数。
胡桃
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.