当前位置: 首页 > 知识库问答 >
问题:

memcpy是一个简单可复制的类型,构造还是赋值?

金兴朝
2023-03-14

假设您有一个T类型的对象和一个适当对齐的内存缓冲区alignas(T)无符号char[sizeof(T)]。如果使用std::memcpyt类型的对象复制到无符号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];
};

编译器可以将Meow的XY存储在缓冲区的任何部分,甚至可以在缓冲区中的随机偏移量处,只要它们正确对齐并且不重叠。如果编译器愿意,XY的偏移量甚至可以随每个构造随机变化。(如果编译器愿意,X可以在Y之后执行,因为标准只要求相同访问说明符的成员按顺序执行,XY具有不同的访问说明符。)

这将满足基本可复制的要求;memcpy将复制隐藏的偏移量字段,因此新的副本将工作。但有些事情行不通。例如,通过memcpy持有指向x的指针将中断:

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

但是,编译器真的允许以这种方式实现一个简单的可复制类吗?只有当a.x的生存期结束时,取消引用px才应该是未定义的行为。有吗?N3797标准草案的相关部分在这个问题上不是很清楚。这是第[basic.life]/1节:

对象的生存期是该对象的运行时属性。如果一个对象属于类或聚合类型,并且它或它的一个成员是由一个构造函数而不是一个普通的默认构造函数初始化的,则该对象被称为具有非普通的初始化。[注意:由普通复制/移动构造函数进行的初始化是非普通初始化。-结束注意]t类型的对象的生存期从以下时间开始:

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

t类型的对象的生存期在以下时间结束:

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

这是[basic.types]/3:

对于任何类型为t的基本可复制对象(基类子对象除外),无论该对象是否具有t类型的有效值,组成该对象的基础字节([intro.memory])都可以复制到char无符号char的数组中。如果charunsigned char数组的内容被复制回对象中,则对象随后将保留其原始值。例略

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

共有1个答案

申屠裕
2023-03-14

对我来说很清楚,使用std::memcpy既不会导致构造也不会导致赋值。它不是构造,因为不会调用任何构造函数。也不是赋值,因为不会调用赋值运算符。如果一个简单可复制的对象具有简单的析构函数、(复制/移动)构造函数和(复制/移动)赋值操作符,那么这一点就没有实际意义了。

您似乎引用了§3.9[Basic.Types]中的第2条。在第3节,它指出:

对于任何简单可复制的类型T,如果指向T的两个指针指向不同的T对象Obj1Obj2,其中Obj1Obj2都不是基类子对象,如果组成Obj1的基础字节(1.7)被复制到Obj2中,41[示例:
  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个无符号char对象的序列,其中N等于sizeof(T)。对象的值表示形式是保存t类型值的位集。对于通常可复制的类型,值表示是对象表示中确定值的一组位,值是实现定义的值集的一个离散元素。42
42)目的是C++的内存模型与ISO/IEC 9899编程语言C的内存模型兼容。

在两个定义的术语前面使用单词The意味着任何给定的类型只有一个对象表示形式,而给定的对象只有一个值表示形式。您假设的变形内部类型不应存在。脚注清楚地表明,其目的是为了让简单的可复制类型具有与C兼容的内存布局。因此,即使是一个具有非标准布局的对象,到处复制它也将允许它可用。

 类似资料:
  • 我很难确定这到底是什么。此时,我已经熟悉了方法、构造函数和类声明的样子。这是哪个?为什么它看起来像一个构造函数和一个方法有一个婴儿?

  • 有人告诉我构造函数复制是一种浅层复制方法。 通常情况下,如果我修改了List ints的第一个元素,那么List intscope中的相应元素应该已经修改为31了,对吗?因为变量intscope和ints引用了同一个对象,但我得到了这个输出。 举个例子,我的意思是: 我得到这样的输出: 因为变量arr和arr2引用了同一个对象。如果我修改了数组中的一个元素,那么其他元素也会产生影响。当我使用列表进

  • 上面引号中加粗的表示对复制构造函数的调用是显式的,对吗?是G++错了还是我对标准的解释错了?

  • 之前我们使用大括号声明并初始化了一些Time结构: Time currentTime = { 9, 14, 30.0 }; Time breadTime = { 3, 35, 0.0 }; 现在,通过使用构造函数,我们能用另一种方式来声明和初始化: Time time (seconds); 这两个函数展现了不同的编程风格,以及在C++历史上不同的观点。可能是出于这个原因,C++编译器要求你使用

  • 问题内容: 这个问题已经在这里有了答案 : 11年前关闭。 精确重复 表命名难题:单数与复数名称 使用单数或复数数据库表名称更好吗?有公认的标准吗? 我听到有人支持和反对它,你们怎么看? 问题答案: 恕我直言,表名称应像客户一样是复数形式。 如果类名映射到“客户”表中的一行,则其名称应与“客户”一样为单数形式。