我可以使用内存写入多个相邻的标准布局子对象

Can I use memcpy to write to multiple adjacent Standard Layout sub-objects?

本文关键字:标准 布局 对象 可以使 内存 我可以      更新时间:2023-10-16

免责声明:这是试图深入研究一个更大的问题,所以请不要纠结于这个例子在实践中是否有意义。

是的,如果你想复制对象,请使用/提供复制构造函数。(但请注意,即使是示例也不会复制整个对象;它尝试在几个相邻的整数(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;
}

考虑到c++标准中的措辞:

9.2类成员

13具有相同访问控制(第11条)的(非联合)类的非静态数据成员被分配,以便后面的成员具有相同的访问控制类对象中的高级地址。(…)实现一致性需求可能导致两个问题相邻的成员不应依次分配;(…)

我可以断定num_1num_3具有递增的地址并且相邻模填充。

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

  • memcpy必须允许以这种方式一次写入多个"内存对象",即特别是

    • 使用num_1的目标地址和大于num_1"对象"大小的大小调用memcpy是合法的。(给定num_1不是数组的一部分)(memcpy(&a + 1, &b + 1,0)在C11中定义吗?似乎是一个很好的相关问题,但不太适合。)
    • c++(14)标准,AFAICT,将memcpy的描述引用到C99标准,并且该标准声明:
    7.21.2.1 memcpy函数

    2 memcpy函数从s2所指向的对象复制n个字符将s1所指向的对象插入

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

    所以那么问题是将三个成员的内存范围重新解释为"概念"(?)字符数组是否合法。

  • 计算sizeAll是合法的,即offsetof的使用是合法的

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

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

§8.5

(6.2) -如果T是(可能是cv限定的)非联合类类型,则每个非静态数据成员和每个基类subbobject为零初始化,填充初始化为零;

标准实际上并没有说这些零比特是可写的,但是我想不出一个架构在内存访问权限上有这种粒度(我们也不希望有)。

所以我想说,在实践中这种重写的零总是安全的,即使当权者没有特别声明。

将三个成员的内存范围重新解释为"概念性"(?)字符数组

是合法的。

不,对象成员的任意子集本身不是任何类型的对象。如果你不能接受sizeof之类的东西,那它就不是一个东西。同样,正如你提供的链接所建议的,如果你不能识别std::is_standard_layout的东西,它就不是一个东西。

类似的是

size_t n = (char*)&num_3 - (char*)&num_1;

它可以编译,但它是UB:减去的指针必须属于同一个对象。

也就是说,即使标准不明确,我认为你在安全范围内。如果MyStandardLayout是标准布局,那么它的子集也是标准布局,即使它没有名称,也不是自己的可识别类型。

但我不会这么做。赋值是绝对安全的,并且可能比内存更快。如果子集是有意义的并且有许多成员,我会考虑将其变成显式结构,并使用赋值而不是memcpy,利用编译器提供的默认成员复制构造函数。

把这个作为部分答案写下来。memcpy(&num_1, buf, sizeAll):

注:James的回答更加简洁和明确。

我问:

  • memcpy必须允许以这种方式一次写入多个"内存对象",即特别是

    • 使用num_1的目标地址和大于num_1"对象"大小的大小调用memcpy是合法的。
    • [c++(14)标准][2],AFAICT,将memcpy的描述引用到[C99标准][3],并且该标准声明:
    7.21.2.1 memcpy函数

    2 memcpy函数从s2所指向的对象复制n个字符将s1所指向的对象插入

    对我来说,这里的问题是。这就是我们这里的目标范围是否可以根据C或c++被视为"对象"标准。

思考和搜索了一点,我发现在C标准:

§6.2.6类型 的表示

§6.2.6.1通用

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

因此至少暗示"一个对象" => "连续的字节序列"。

我不敢大胆地断言逆- "连续的字节序列" => "一个对象" -成立,但至少"一个对象"在这里似乎没有更严格的定义。

然后,如Q中引用的,c++标准的§9.2/13(和§1.8/5)似乎保证我们确实有一个连续的字节序列(包括填充)。

那么,§3.9/3说:

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

T* t1p;
T* t2p;       
     // provided that t2p points to an initialized object ...         
std::memcpy(t1p, t2p, sizeof(T));  
     // at this point, every subobject of trivially copyable type in *t1p contains        
     // the same value as the corresponding subobject in *t2p

-end example]

因此这明确地允许memcpy应用于 trivitically Copyable类型的整个对象。

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

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

_Composite中的nums_Flat中的三个成员的内存布局应该完全相同,因为使用相同的基本规则。

因此在结论中,假设"子对象"num_1到num_3将被等效的连续字节序列表示为完整的Trivially Copyable子对象,I:

  • 有一个非常非常很难想象一个实现或优化器打破这个
  • 会说它可以是:
    • 读取为未定义行为,如果,我们得出结论,c++§3.9/3暗示只有(完整的)Trivially Copyable类型的对象才允许被memcpy这样处理,或者从C99§6.2.6.1/2和memcpy 7.21.2.1的通用规范中得出结论,num_*字节的连续序列不包含用于memcopy的"有效对象"。
    • 读取为定义的行为,如果,我们得出结论,c++§3.9/3没有规范地限制memcpy对其他类型或内存范围的适用性,并得出结论,C99标准中的memcpy(和"对象术语")的定义允许将相邻变量视为单个对象连续字节目标。