【问题标题】:What's the c++'s default assign operation behavior?c++ 的默认分配操作行为是什么?
【发布时间】:2013-08-07 10:27:10
【问题描述】:

例如,这让我很困惑:

struct A {
//  some fileds...
    char buf[SIZE];
};

A a;
a = a;

通过A的字段buf,看起来可能默认的赋值操作会调用memcpy之类的东西来将对象X赋值给Y,那么如果将对象赋值给自己并且没有定义明确的赋值操作怎么办,比如上面的a = a;

memcpy 手册页:

DESCRIPTION

The  memcpy() function copies n bytes from memory area src to memory area dest.  The memory areas must not overlap.  Use memmove(3) if the memory areas do overlap.

如果使用memcpy,可能会出现一些未定义的行为。

那么,C++ 对象中默认的赋值操作行为是什么?

【问题讨论】:

  • 重叠的问题是,如果您从一个被该区域的另一部分覆盖的区域复制 - 也就是说,您将一个字符插入到字符串中,然后执行memcpy(&a[index+1], &a[index], len-index);。但是memcpy(a, a, sizeof(a)) 会很好。
  • @R.MartinhoFernandes 你能这么好心告诉我那个例外是什么吗?
  • @nijansen 这是字符串文字。 "bar""foobar" 是可以重叠存储的一对文字的示例。 (在这种情况下这不是问题,因为这些数组是 const。)
  • @MatsPetersson memcpy(a, a, sizeof(a)) 不好,src 和 dst(完全)重叠。
  • 好吧,让我换个说法:它可能是未定义的,但重叠区域的危险不是当重叠完成时,也就是说,你正在将东西从位置 X 复制到位置 X,但是当源和目标之间存在偏移时,源、源+长度和目标、目标+长度重叠,因为目标的写入将在某些时候覆盖源。如果您直接覆盖,则导致问题的可能性要小得多。当然,在自身上复制 200 个字节也是完全浪费时间。

标签: c++ assignment-operator


【解决方案1】:

赋值运算符不是根据memcpy (§12.8/28) 定义的。

为非联合隐式定义的复制/移动赋值运算符 X 类执行其子对象的成员复制/移动分配。 首先分配 X 的直接基类,按照它们的顺序 在基本说明符列表中声明,然后是立即数 X 的非静态数据成员按它们分配的顺序分配 在类定义中声明。让 x 成为参数 函数的值,或者,对于移动运算符,一个 xvalue 指的是 范围。每个子对象都以适合其的方式分配 类型:

[...]

——如果子对象是一个数组,每个元素都被赋值,在 适合元素类型的方式;

[...]

如您所见,每个char 元素都将被单独分配。那总是安全的。

但是,根据 as-if 规则,编译器可以用 memmove 替换它,因为它与 char 数组具有相同的行为。如果它可以保证memcpy 会导致相同的行为,它也可以用memcpy 替换它,即使理论上这样的事情是未定义的。编译器可以依赖理论上未定义的行为;存在未定义行为的原因之一是编译器可以将其定义为更适合其操作的​​任何内容。

实际上,在这种情况下,编译器可以进一步采用 as-if 规则,并且根本不对数组做任何事情,因为这也会导致相同的行为。

【讨论】:

    【解决方案2】:

    默认分配(和复制)行为不会 memcpy 整个类,这会破坏事情。每个成员都使用其复制构造函数或赋值运算符(取决于操作)进行复制。这以递归方式应用于成员及其成员。当达到基本数据类型时,它只是执行数据的直接复制,类似于 memcpy。因此,可以像 memcpy 一样复制基本数据类型的数组,但整个类不是。如果你将 std::string 添加到你的类中,它的 = 运算符将被调用,以及数组的副本。如果您使用 std::string 数组,则数组中的每个字符串都会调用它们的运算符。他们不会 memcpy。

    【讨论】:

    • 该答案部分正确,但不包括 OP 询问的具体情况。
    • 由于没有使用 memcpy,因此违反 memcpy 规则不再相关。但我应该讨论一下自我分配。
    • 其实我不确定默认操作符如何处理自赋值。我的直觉是它没有明确地,并且依赖于必须阻止这种事情的成员,在他们的自定义赋值运算符中进行自己的检查。
    • @NeilKirk 为什么编译器应该在自赋值方面做一些特别的事情? (就此而言,用户定义的赋值运算符为什么要担心自赋值?)
    • @NeilKirk 如果您在网上搜索,您会发现很多错误信息。通常,如果您需要检查自赋值,则赋值运算符已损坏。 (并且 std::vector<>::operator= 的正确编写实现不需要检查自分配。尽管可能出于优化的原因;向量分配非常昂贵。)
    【解决方案3】:

    一些有限的实验告诉我,g++ 完全消除了复制a = a; 的任何尝试[假设这很明显——我敢肯定,如果指针足够混乱,最终将有可能在自身上复制相同的对象,并且获得未定义的行为]。

    【讨论】:

    • 它不会得到UB,它是按照element-wise copy定义的(§12.8,28),并且element-wise self-assignment是很好定义的。
    【解决方案4】:

    如果使用 memcpy,可能会出现一些未定义的行为。

    这是一个如何复制给定类的实现细节。 memcpy() 函数和复制构造函数都会被转换成一些机器码。但是,您在内存中的对象不应重叠,因为默认分配并不能保证在它们重叠的情况下您将获得正确的结果。

    那么,C++ 对象中默认的赋值操作行为是什么?

    与其他响应一样,行为会递归调用所有类/结构成员的赋值。但是从技术上讲,就像您的情况一样,它可能只是复制整个内存块,特别是如果您的结构是 POD(普通旧数据)。

    【讨论】:

    • 这是错误的。隐式复制构造函数可以很好地使用自赋值。 (这无关紧要,示例是关于赋值而不是初始化)
    • 到底出了什么问题?我指的是重叠的对象,而不是自我分配。对于重叠的对象,行为是不确定的。
    • 复制构造函数?不,不是。 (如何调用具有重叠对象的复制构造函数?)
    • 对,我错误地提到了复制构造函数,它必须是“默认赋值”。会改正的,谢谢。尽管在重叠的内存块上连续使用 placemnent new 时,复制构造函数也可能表现不正确。
    猜你喜欢
    • 2014-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-10
    • 1970-01-01
    • 2011-05-27
    • 1970-01-01
    • 2010-11-04
    相关资源
    最近更新 更多