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

时间:2016-08-18 20:23:19

标签: c++ language-lawyer memcpy

免责声明:这是在尝试深入研究一个更大的问题,所以请不要对这个例子在实践中是否有任何意义感到不知所措。

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

鉴于C ++ Standard Layout 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++ Standard中的措辞:

  

9.2班级成员

     

...

     

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

我的结论是,保证num_1num_3的地址越来越大,并且模数填充相邻。

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

    必须允许
  • memcpy写入多个&#34;内存对象&#34;以这种方式立刻,即特别是

      

    7.21.2.1 memcpy函数

         

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

    对我来说,问题就在这里。这是我们这里的目标范围是否可以被认为是#34;一个对象&#34;根据C或C ++标准。注意:声明和定义的一个(部分)字符数组(当然)可以假设为&#34;一个对象&#34;出于memcpy的目的,因为我非常确定我可以从char数组的一部分复制到(另一个)char数组的另一部分。

    那么然后问题是,如果将三个成员的内存范围重新解释为&#34;概念性&#34;(?)字符数组是合法的。

    < / LI>
  • 计算sizeAll是合法的,offsetof的使用是合法的,如图所示。

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

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

3 个答案:

答案 0 :(得分:3)

第8.5节

  

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

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

所以我会说在实践中这个重写零将永远是安全的,即使没有被Beers那样的具体声明。

答案 1 :(得分:2)

  

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

不,对象成员的任意子集本身不是任何类型的对象。如果你不能拿sizeof的东西,那不是一回事。同样,正如您提供的链接所示,如果您无法识别std::is_standard_layout的内容,那么这不是一件事。

类比将是

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

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

那就是说,即使标准不明确,我认为你处于安全的境地。如果MyStandardLayout是标准布局,那么即使它没有名称且不是它自己的可识别类型,也可以说它的一个子集也是如此。

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

答案 2 :(得分:1)

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

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

我问道:

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

         
        
    • 使用memcpy的目标地址调用num_1并且大小超过num_1“对象的大小”是合法的。
    •   
    • [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 除了位字段外,对象由一个或多个字节的连续序列组成,数字,顺序和编码   它们是明确指定的或实现定义的。

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

我不是那么大胆地​​声称反向 - “连续的字节序列”=&gt; “一个对象” - 持有,但至少“一个对象”似乎没有在这里更严格地定义。

然后,如Q中所引用的,C ++标准的§9.2/ 13(以及§1.8/ 5)似乎保证我们具有连续的字节序列(包括填充)。< / p>

然后,§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
     

-end example]

因此,这明确允许将memcpy应用于 Trivially 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;
};

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

所以总结,假设“子对象”num_1到num_3将由等效的连续字节序列表示为完全的Trivially Copyable子对象,我:

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