为了账号安全,请及时绑定邮箱和手机立即绑定

使用memcpy“构造”一个​​平凡可复制的对象

/ 猿问

使用memcpy“构造”一个​​平凡可复制的对象

C++
芜湖不芜 2019-11-19 15:25:55

在C ++中,此代码正确吗?


#include <cstdlib>

#include <cstring>


struct T   // trivially copyable type

{

    int x, y;

};


int main()

{

    void *buf = std::malloc( sizeof(T) );

    if ( !buf ) return 0;


    T a{};

    std::memcpy(buf, &a, sizeof a);

    T *b = static_cast<T *>(buf);


    b->x = b->y;


    free(buf);

}

换句话说,是否是*b一个寿命已经开始的物体?(如果是这样,它什么时候开始的?)


查看完整描述

3 回答

?
沧海一幻觉

    n3751支持以下未指定的操作:对象生存期,低级编程和memcpy,其中包括:


C ++标准目前对使用memcpy复制对象表示字节在概念上是赋值还是对象构造都保持沉默。对于基于语义的程序分析和转换工具以及跟踪对象生存期的优化程序,差异确实很重要。本文建议


允许使用memcpy复制两个不同的平凡可复制表(但是大小相同)的两个不同对象的字节


这样的使用被识别为初始化,或更普遍地被识别为(概念上)对象构造。


识别为对象构造将支持二进制IO,同时仍允许基于生命周期的分析和优化器。


我找不到本文讨论过的任何会议纪要,因此似乎仍然是一个未解决的问题。


C ++ 14草案标准目前在1.8 [intro.object]中说:


[...]在需要时,通过定义(3.1),新表达式(5.3.4)或实现(12.2)创建对象。[...]


这是我们所没有的malloc,而复制琐碎可复制类型的标准中所涉及的情况似乎仅引用3.9 [basic.types]部分中已存在的对象:


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


和:


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


提案基本上是这样说的,因此这不足为奇。


dyp从ub邮件列表中指出了关于该主题的有趣讨论:[ub]键入punning以避免复制。


投标p0593:隐式创建对象以进行低级对象操作

提案p0593试图解决此问题,但AFAIK尚未得到审查。


本文建议在新分配的存储中根据需要按需创建足够琐碎类型的对象,以赋予程序定义的行为。


它有一些激励性的示例,它们在本质上相似,包括当前的std :: vector实现,该实现当前具有未定义的行为。


它提出了以下几种隐式创建对象的方法:


我们建议至少将以下操作指定为隐式创建对象:


char,unsigned char或std :: byte数组的创建在该数组中隐式创建对象。


对malloc,calloc,realloc或任何名为operator new或operator new []的函数的调用会在其返回的存储中隐式创建对象。


std :: allocator ::: allocate同样在返回的存储中隐式创建对象;分配器要求应该要求其他分配器实现也要这样做。


调用memmove的行为就像


将源存储复制到临时区域


在目标存储中隐式创建对象,然后


将临时存储复制到目标存储。


这允许记忆保留普通复制对象的类型,或用于将一个对象的字节表示重新解释为另一对象的字节表示。


调用memcpy的行为与调用memmove的行为相同,不同之处在于它在源和目标之间引入了重叠限制。


提名联合成员的类成员访问将触发由联合成员占用的存储区中的隐式对象创建。请注意,这不是一个全新的规则:[P0137R1]中已经存在此权限,其中成员访问位于分配的左侧,但现在已被概括为该新框架的一部分。如下所述,这不允许通过联合进行类型修剪。相反,它仅允许通过类成员访问表达式来更改活动的联合成员。


应将新的屏障操作(不同于不会创建对象的std :: launder)引入标准库,其语义等效于具有相同源存储和目标存储的内存。作为一名稻草人,我们建议:


// Requires: [start, (char*)start + length) denotes a region of allocated

// storage that is a subset of the region of storage reachable through start.

// Effects: implicitly creates objects within the denoted region.

void std::bless(void *start, size_t length);

除上述内容外,还应将实现标准定义的非标准内存分配和映射功能集(例如POSIX系统上的mmap和Windows系统上的VirtualAlloc)指定为隐式创建对象。


注意,指针reinterpret_cast不足以触发隐式对象创建。


查看完整回答
反对 回复 2019-11-19
?
qq_遁去的一_1

从快速搜索。


“ ...生存期从为对象分配了正确对齐的存储开始,到存储被另一个对象释放或重新使用时结束。”


因此,我要说的是,生存期以分配开始,以免费结束。


查看完整回答
反对 回复 2019-11-19
?
慕村9548890

此代码正确吗?


好吧,它通常会“起作用”,但仅适用于琐碎的类型。


我知道您没有要求它,但是让我们使用一个非平凡类型的示例:


#include <cstdlib>

#include <cstring>

#include <string>


struct T   // trivially copyable type

{

    std::string x, y;

};


int main()

{

    void *buf = std::malloc( sizeof(T) );

    if ( !buf ) return 0;


    T a{};

    a.x = "test";


    std::memcpy(buf, &a, sizeof a);    

    T *b = static_cast<T *>(buf);


    b->x = b->y;


    free(buf);

}

构造后a,a.x将分配一个值。假设std::string未针对使用较小字符串值的本地缓冲区进行优化,而只是将数据指针指向外部存储块。将原样memcpy()的内部数据复制a到中buf。现在a.x,b->x为string数据引用相同的内存地址。当b->x分配一个新值时,该存储块将被释放,但a.x仍会引用它。当a随后在年底进入的范围了main(),它会尝试再次释放相同的内存块。发生未定义的行为。


如果要“正确”,将对象构造到现有内存块中的正确方法是改用placement-new运算符,例如:


#include <cstdlib>

#include <cstring>


struct T   // does not have to be trivially copyable

{

    // any members

};


int main()

{

    void *buf = std::malloc( sizeof(T) );

    if ( !buf ) return 0;


    T *b = new(buf) T; // <- placement-new

    // calls the T() constructor, which in turn calls

    // all member constructors...


    // b is a valid self-contained object,

    // use as needed...


    b->~T(); // <-- no placement-delete, must call the destructor explicitly

    free(buf);

}


查看完整回答
反对 回复 2019-11-19

添加回答

回复

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信