我可以使用memcpy写入多个相邻的标准布局子对象吗?

Mar*_* Ba 6 c++ memcpy language-lawyer

免责声明:这是试图深入研究一个更大的问题,所以请不要挂断该示例在实践中是否有任何意义.

并且,是的,如果要复制对象,请使用/提供复制构造函数.(但请注意,即使该示例也不会复制整个对象;它会尝试在几个相邻(Q.2)整数上显示一些内存.)


给定C++ 标准布局 struct,我可以使用一次memcpy写入多个(相邻)子对象吗?

完整示例:(https://ideone.com/1lP2Gd https://ideone.com/YXspBk)

#include <vector>
#include <iostream>
#include <assert.h>
#include <inttypes.h>
#include <stddef.h>
#include <memory.h>

struct MyStandardLayout {
    char mem_a;
    int16_t num_1;
    int32_t num_2;
    int64_t num_3;
    char mem_z;

    MyStandardLayout()
    : mem_a('a')
    , num_1(1 + (1 << 14))
    , num_2(1 + (1 << 30))
    , num_3(1LL + (1LL << 62))
    , mem_z('z')
    { }

    void print() const {
        std::cout << 
            "MySL Obj: " <<
            mem_a << " / " <<
            num_1 << " / " <<
            num_2 << " / " <<
            num_3 << " / " <<
            mem_z << "\n";
    }
};

void ZeroInts(MyStandardLayout* pObj) {
    const size_t first = offsetof(MyStandardLayout, num_1);
    const size_t third = offsetof(MyStandardLayout, num_3);
    std::cout << "ofs(1st) =  " << first << "\n";
    std::cout << "ofs(3rd) =  " << third << "\n";
    assert(third > first);
    const size_t delta = third - first;
    std::cout << "delta =  " << delta << "\n";
    const size_t sizeAll = delta + sizeof(MyStandardLayout::num_3);
    std::cout << "sizeAll =  " << sizeAll << "\n";

    std::vector<char> buf( sizeAll, 0 );
    memcpy(&pObj->num_1, &buf[0], sizeAll);
}

int main()
{
    MyStandardLayout obj;
    obj.print();
    ZeroInts(&obj);
    obj.print();

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

鉴于C++标准中的措辞:

9.2班级成员

...

13分配具有相同访问控制(第11条)的(非联合)类的非静态数据成员,以便后面的成员在类对象中具有更高的地址.(......)实施一致性要求可能导致两个相邻成员不能立即分配; (......)

我的结论是可以保证num_1num_3有增加的地址和相邻模填充.

为了完全定义上面的例子,我看到了这些要求,我不确定它们是否适用:

  • memcpy 必须允许一次以这种方式写入多个"存储器对象",即具体而言

    7.21.2.1 memcpy函数

    2 memcpy函数将s2指向的对象中的n个字符复制到s1指向的对象中.

    所以对我来说问题就在这里.这是根据C或C++标准,我们这里的目标范围是否可以被视为"对象".注意:声明和定义的字符数组(的一部分)当然可以假定为"对象",memcpy因为我很确定我可以从一个部分复制char数组到(另一个)char数组的另一部分.

    所以,那么问题是,如果是合法的重新诠释三个成员的内存范围作为一个"概念"(?)字符数组.

  • 计算sizeAll是合法的,即使用offsetof是合法的,如图所示.

  • 写入成员之间的填充是合法的.

这些属性是否成立?我错过了什么吗?

Mar*_* Ba 1

将其作为部分答案。memcpy(&num_1, buf, sizeAll)

\n\n

注意:詹姆斯的回答更加简洁和明确。

\n\n

我问:

\n\n
\n
    \n
  • memcpy必须允许以这种方式一次写入多个“内存对象”,即具体而言

    \n\n
      \n
    • memcpy使用目标地址num_1和大于“对象”大小的大小进行调用num_1是合法的。
    • \n
    • [C++ (14) 标准][2],AFAICT,引用了memcpy[C99 标准][3] 的描述,其中指出:
    • \n
    \n\n
    \n

    7.21.2.1 memcpy 函数

    \n\n

    2 memcpy 函数将 s2 \n 指向的对象中的 n 个字符复制到s1 指向的对象中。

    \n
    \n\n

    所以对我来说这里的问题是。这是根据 C 或 C++\n 标准,我们这里的目标范围是否可以被视为“对象”。

  • \n
\n
\n\n

经过思考和搜索,我在 C 标准中找到了:

\n\n
\n

\xc2\xa7 6.2.6 类型的表示

\n\n

\xc2\xa7 6.2.6.1 概述

\n\n

2除位域外,对象由一个或多个字节的连续序列组成,其数量、顺序和编码要么是显式指定的,要么是实现定义的。

\n
\n\n

所以至少暗示“一个对象”=>“连续的字节序列”。

\n\n

我并没有那么大胆地​​宣称逆向——“连续的字节序列”=>“一个对象”——成立,但至少“一个对象”在这里似乎没有更严格的定义。

\n\n

然后,正如 Q 中引用的,C++ 标准的 \xc2\xa79.2/13 (和 \xc2\xa7 1.8/5)似乎保证我们确实一个连续的字节序列(包括填充)。

\n\n

然后,\xc2\xa73.9/3 说:

\n\n
\n

3对于任何普通可复制类型 T,如果指向 T 的两个指针指向不同的 T 对象 obj1 和 obj2,其中 obj1 和 obj2 都不是基类子对象,如果构成 obj1 的基础字节 (1.7) 是\ n 复制到 obj2 中,obj2 随后应保持与 obj1 相同的值。\n [ 示例:

\n\n
T* t1p;\nT* t2p;       \n     // provided that t2p points to an initialized object ...         \nstd::memcpy(t1p, t2p, sizeof(T));  \n     // at this point, every subobject of trivially copyable type in *t1p contains        \n     // the same value as the corresponding subobject in *t2p\n
Run Code Online (Sandbox Code Playgroud)\n\n

\xe2\x80\x94结束示例]

\n
\n\n

因此,这明确允许应用memcpy普通可复制类型的整个对象。

\n\n

在示例中,这三个成员组成了一个“可简单复制的子对象”,实际上,我认为将它们包装在不同类型的实际子对象中仍然会要求显式对象与这三个成员具有完全相同的内存布局:

\n\n
struct MyStandardLayout_Flat {\n    char mem_a;\n    int16_t num_1;\n    int32_t num_2;\n    int64_t num_3;\n    char mem_z;\n};\n\nstruct MyStandardLayout_Sub {\n    int16_t num_1;\n    int32_t num_2;\n    int64_t num_3;\n};\n\nstruct MyStandardLayout_Composite {\n    char mem_a;\n    // Note that the padding here is different from the padding in MyStandardLayout_Flat, but that doesn\'t change how num_* are layed out.\n    MyStandardLayout_Sub nums;\n    char mem_z;\n};\n
Run Code Online (Sandbox Code Playgroud)\n\n

numsin_Composite和 的三个成员的内存布局_Flat应该完全相同,因为适用相同的基本规则。

\n\n

所以总而言之,假设“子对象”num_1 到 num_3 将由等效的连续字节序列表示为完整的可简单复制子对象,我:

\n\n
    \n
  • 很难想象有一个实现或优化器可以打破这个问题
  • \n
  • 会说它可以是:\n\n
      \n
    • 读为未定义行为,当且仅当我们得出结论 C++\xc2\xa73.9/3 意味着只有普通可复制类型的(完整)对象才允许被memcpyC99\xc2\xa76.2.6.1/ 如此处理或得出结论2 和 7.21.2.1 的一般规范memcpy,即 num_* 字节的连续序列不包含用于 memcopy 目的的“有效对象”。
    • \n
    • 读作定义的行为,当且仅当我们得出结论,C++\xc2\xa73.9/3 没有规范地限制memcpy到其他类型或内存范围的适用性,并得出结论,C99 标准中的定义memcpy(和“对象术语”)允许将相邻变量视为单个对象连续字节目标。
    • \n
  • \n
\n