【问题标题】:Can I use memcpy to write to multiple adjacent Standard Layout sub-objects?我可以使用 memcpy 写入多个相邻的标准布局子对象吗?
【发布时间】:2016-12-25 21:30:50
【问题描述】:

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

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


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

完整示例:(https://ideone.com/1lP2Gdhttps://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++ Standard中的措辞:

9.2 类成员

...

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

我会得出结论,可以保证num_1num_3 具有增加的地址并且是相邻的模填充。

为了完全定义上述示例,我看到了这些要求,但我不确定它们是否满足:

  • memcpy必须被允许以这种方式一次写入多个“内存对象”,即明确

    7.21.2.1 memcpy 函数

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

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

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

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

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

这些属性是否成立?我还有什么遗漏吗?

【问题讨论】:

  • 在这种情况下不要使用memcpy()std::copy()。改为提供复制构造函数。
  • @πάνταῥεῖ - 你知道,我确实包含了免责声明,并且问题包含 [lang-lawyer] 标签。这是 SW 专业人士的网站。我不需要包括“不要在家里做这个,孩子们”的标志,还是我? :-P
  • delta 的计算是未定义的,因为指针不指向同一个数组。 (第 5.7 节第 6 段最后一句。)
  • @MartinBa “这些属性是否成立?” 它从来都不适用于任何标准,这也没有改变。您的标记没有帮助。
  • @MartinBa 我已经提到了 C++11 标准。

标签: c++ language-lawyer memcpy


【解决方案1】:

§8.5

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

现在标准实际上并没有说这些零位是可写的,但我想不出一种在内存访问权限上具有这种粒度级别的架构(我们也不想这样做)。

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

【讨论】:

  • 不错的收获!我很惊讶标准明确定义了填充的值。不过,Grepping PDF 并没有对我的原因产生任何进一步的了解。
  • "现在标准实际上并没有说这些零位是可写的" 确实如此。能够memcpy 在琐碎的可复制类型之间是不可避免的结果。
  • @NicolBolas 啊,是的,你当然是对的。这现在引出了一个问题,如果这些填充位中的任何一个被赋予非零值(例如作为 memcpy 的结果),它是否是“定义的行为”。
  • @RichardHodges: memcpy 并不是为这些字节赋予非零值的唯一方法。原始内存分配的默认初始化也可以做到这一点。除非您认为执行 new C 会清除 just 填充。标准中没有任何内容要求填充字节具有任何特定值。
  • @NicolBolas - 我当然会在实践中同意,但我不确定来自 Std POV:memcpy wrt 的要求。 TC 类型不暗示任何关于填充的内容,因此在之前或之后,填充值都不是由§3.9 : 2, 3 定义的。它谈论“原始价值”和“相同价值” - 我不确定填充是否可以被视为价值的一部分。 (同样,理论上来说。)
【解决方案2】:

将三个成员的内存范围重新解释为“概念”(?)char数组是合法的

不,对象成员的任意子集本身并不是任何类型的对象。如果你不能拿sizeof 东西,那不是东西。同样,正如您提供的链接所建议的那样,如果您无法将事物识别到std::is_standard_layout,则它不是事物。

类似

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

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

也就是说,即使标准不明确,我认为您仍处于安全区域。如果MyStandardLayout 是一个标准布局,那么它的一个子集也是合理的,即使它没有名称并且不是它自己的可识别类型。

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

【讨论】:

  • 谢谢。很好的接受。关于我的考虑,请参阅我太长的答案。这个问题。
  • “是标准布局,按理说它的一个子集也是” - 我会说它实际上是强制标准中的 SL 规则标准布局类型的“非正式”复合子对象与显式复合子对象相同。 (§9.2/13)尽管如此,是否有任何后续问题是另一个问题:-)
【解决方案3】:

将此作为部分答案。 memcpy(&amp;num_1, buf, sizeAll):

注意:James' answer 更加简洁和明确。

我问:

  • 必须允许memcpy一次以这种方式写入多个“内存对象”,即明确

    • 使用目标地址num_1 和大于num_1“对象”大小的大小调用memcpy 是合法的。
    • [C++ (14) 标准][2],AFAICT,将memcpy 的描述参考 [C99 标准][3],其中一项指出:

    7.21.2.1 memcpy 函数

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

    所以对我来说这里的问题是 wrt。这就是我们在这里拥有的目标范围是否可以根据 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 都不是 基类子对象,如果构成 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

——结束示例]

因此,这明确允许将memcpy 应用于可轻松复制类型的整个对象。

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

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 子对象,我:

  • 很难非常非常想出一个可以打破这一点的实现或优化器
  • 可以说它可以是:
    • 读作 Undefined 行为,iff 我们得出结论,C++§3.9/3 暗示 只有(完整)可简单复制类型的对象是允许由memcpy 如此对待或从 C99§6.2.6.1/2 和memcpy 7.21.2.1 的一般规范得出结论,num_* 字节的连续序列不包含memcopy 的用途。
    • 读作已定义行为,iff我们得出结论,C++§3.9/3 并未规范地将memcpy 的适用性限制为其他类型或内存范围,并得出结论: C99 标准中memcpy(和“对象术语”)的定义允许将相邻变量视为单个对象连续字节目标。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多