首页 文章

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

提问于
浏览
26

假设您有一个 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];
};

编译器可以在 buffer 的任何部分将缓冲区中的 xy 存储在缓冲区中,甚至可以在 buffer 内的随机偏移处存储,只要它们正确对齐且不重叠即可 . 如果编译器希望, 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

但是,编译器是否真的允许以这种方式实现一个简单的可复制类?解除引用 px 应该只是未定义的行为,如果 a.x 's lifetime has ended. Has it? The relevant portions of the N3797 draft Standard aren't非常明确的主题 . 这是 [basic.life]/1 部分:

对象的生命周期是对象的运行时属性 . 如果一个对象属于类或聚合类型,并且它或其成员之一由除了普通默认构造函数之外的构造函数初始化,则称该对象具有非平凡的初始化 . [注意:通过简单的复制/移动构造函数进行初始化是非平凡的初始化 . - 结束注释]类型T的对象的生命周期开始于:获得具有适当对齐和类型T大小的存储,并且如果对象具有非平凡的初始化,则其初始化完成 . 类型为T的对象的生命周期结束时:如果T是具有非平凡析构函数([class.dtor])的类类型,则析构函数调用将启动,或者对象占用的存储将被重用或释放 .

这是 [basic.types]/3

对于普通可复制类型T的任何对象(基类子对象除外),无论对象是否包含类型T的有效值,组成对象的基础字节([intro.memory])都可以复制到char或unsigned char数组 . 如果将char或unsigned char数组的内容复制回对象,则该对象应随后保持其原始值 . 示例省略

那么问题就是 memcpy 覆盖了一个简单的可复制的类实例"copy construction"或"copy-assignment"?这个问题的答案似乎决定 Meow_internal 是否是编译器实现可复制类 Meow 的有效方法 .

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

2 回答

  • 2

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

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

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

    显然,该标准旨在允许 *t1p 以各种方式使用 *t1p .

    继续到¶4:

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

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

  • 6

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

    对于任何简单的可复制类型T,如果指向T的两个指针指向不同的T对象obj1和obj2,其中obj1和obj2都不是基类子对象,如果组成obj1的基础字节(1.7)被复制到obj2,obj2随后应保持与obj1相同的值 .

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

相关问题