memcpy是一个简单的可复制类型的构造或作业?

时间:2014-10-03 00:53:12

标签: c++ c++11 copy-constructor language-lawyer memcpy

假设您有一个T类型的对象和一个适当对齐的内存缓冲区alignas(T) unsigned char[sizeof(T)]。如果您使用std::memcpyT类型的对象复制到unsigned char数组,是否考虑复制构造或复制分配?

如果一个类型可以轻易复制而不是标准布局,那么可以设想这样的类:

struct Meow
{
    int x;
protected: // different access-specifier means not standard-layout
    int y;
};

可以像这样实现,因为编译器不会强制使用标准布局:

struct Meow_internal
{
private:
    ptrdiff_t x_offset;
    ptrdiff_t y_offset;
    unsigned char buffer[sizeof(int) * 2 + ANY_CONSTANT];
};

编译器可以在x的任何部分将缓冲区的ybuffer存储在缓冲区内,甚至可以在buffer内的随机偏移处存储,只要它们是x。正确对齐并且不重叠。如果编译器希望,yx的偏移量甚至可以随每个结构随机变化。 (y如果编译器希望x可以使用,因为标准只要求相同访问说明符的成员按顺序排列,ymemcpy具有不同的访问权限-specifiers。)

这符合可轻易复制的要求; x将复制隐藏的偏移字段,因此新副本可以正常工作。但有些事情是行不通的。例如,在memcpy上方指向Meow a; a.x = 2; a.y = 4; int *px = &a.x; Meow b; b.x = 3; b.y = 9; std::memcpy(&a, &b, sizeof(a)); ++*px; // kaboom 的指针会中断:

px

但是,编译器是否真的允许以这种方式实现一个简单的可复制类?如果a.x的生命周期结束,解除引用T应该只是未定义的行为。有吗? N3797标准草案的相关部分在这个问题上并不十分清楚。这是 [basic.life] / 1

部分
  

对象的生存期是对象的运行时属性。一个   如果对象属于类,则称其具有非平凡的初始化   或者聚合类型,它或它的一个成员由a初始化   除了一个普通的默认构造函数之外的构造函数。 [注意:   通过简单的复制/移动构造函数进行初始化是非常重要的   初始化。 - 结束注释] T类型对象的生命周期   从以下时间开始:

     
      
  • 获得具有T类型的正确对齐和大小的存储,并且
  •   
  • 如果对象具有非平凡的初始化,则其初始化完成。
  •   
     

类型T的对象的生命周期在以下时间结束:

     
      
  • 如果T是具有非平凡析构函数的类类型( [class.dtor] ),则析构函数调用将启动,或者
  •   
  • 对象占用的存储空间被重用或释放。
  •   

这是 [basic.types] / 3

  

对于任何对象(基类子对象除外)的简单   可复制类型T,无论对象是否包含有效值   键入char,组成的基础字节( [intro.memory] ​​)   可以将对象复制到unsigned charchar的数组中。如果   将unsigned charmemcpy数组的内容复制回来   进入物体后,物体应随后保持其原始状态   值。 示例省略

然后问题是,Meow_internal是否覆盖了一个可复制的类实例“复制构造”或“复制分配”?这个问题的答案似乎决定了Meow是否是编译器实现可复制的类memcpy的有效方式。

如果Meow_internal是“复制构造”,则答案是memcpy有效,因为复制构造正在重用内存。如果Meow_internal是“copy-assignment”,那么答案是memcpy不是有效的实现,因为赋值不会使指向实例化类的成员的指针无效。如果两者都是{{1}},我不知道答案是什么。

2 个答案:

答案 0 :(得分:6)

我很清楚,使用std::memcpy不会导致构造或分配。它不是构造,因为不会调用构造函数。也不是赋值,因为不会调用赋值运算符。鉴于一个简单的可复制对象具有简单的析构函数,(复制/移动)构造函数和(复制/移动)赋值运算符,这一点非常没有用。

你似乎引用了§3.9[basic.types]中的¶2。在¶3,它声明:

  

对于任何简单的可复制类型T,如果指向T的两个指针指向不同的T对象obj1obj2,则obj1如果构成obj2的基础字节(1.7)被复制到obj1 41 obj2,则obj2是基类子对象随后应保持与obj1相同的值。 [例如:
  T* t1p;
  T* t2p;
  // ,前提是 t2p 指向初始化对象......
  std::memcpy(t1p, t2p, sizeof(T));
  // 此时, *t1p 中的每个可复制类型的子对象都包含
  // *t2p中相应的子对象相同的值    - 结束示例]
   41)例如,使用库函数(17.6.1.2)std::memcpystd::memmove

显然,该标准旨在允许*t1p*t2p的各种方式使用{。}}。

继续¶4:

  

类型为T的对象的对象表示是由T类型的对象占用的 N 无符号字符对象的序列,其中 N 等于sizeof(T)。对象的值表示是保存类型T的值的位集。对于简单的可复制类型,值表示是对象表示中的一组位,用于确定一个值,该值是实现定义的一组值的一个离散元素。 42
   42)意图是C ++的内存模型与ISO / IEC 9899编程语言C的内存模型兼容。

在两个定义的术语前面使用 这个词意味着任何给定的类型只有 一个 对象表示和给定对象只有 一个 值表示。您的假设变形内部类型不应存在。脚注清楚地表明,对于具有与C兼容的存储器布局,其意图是对于具有非标准布局的对象,即使是具有非标准布局的对象,复制它仍然允许它可用。

答案 1 :(得分:2)

在同一稿中,您还可以直接在您引用的文字后面找到以下文字:

  

对于任何简单的可复制类型T,如果指向T的两个指针指向不同的obj1个对象obj2obj1,其中   如果复制构成obj2的基础字节(1.7),则obj1obj2都不是基类子对象   进入obj2obj1随后应与obj2保持相同的值。

请注意,这说明obj2的值的更改,而不是销毁对象{{1}}并在其位置创建新对象。由于不是对象,而只是其值被更改,因此对其成员的任何指针或引用都应保持有效。