问题 用memcpy“构造”一个​​可复制的对象


在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 一个生命已经开始的物体? (如果是的话,它什么时候开始呢?)


2353
2018-05-08 01:35


起源

有关: stackoverflow.com/questions/26171827/... - M.M
我能想到的唯一潜在问题是 严格别名。您可以通过更改类型来纠正 buf 在这种情况下,我会说两者 b 和 buff 是同一个,ergo具有相同的寿命。 - nonsensickle
@nonsensickle我认为严格的别名不适用于此:if *b 是一个类型的对象 T 然后就没有使用它的别名违规;如果不是那么它是UB因为 b->y 尝试读取不存在的对象。当然,改变它的类型没有任何区别 buf;转换指针不会改变它指向的对象的动态类型 - M.M
是的,我认为你是对的。只要你不使用 buf 作为一个 左值 它不应该违反严格的别名规则。我撤回了我的论点,但如果你不介意我会留下评论。 - nonsensickle


答案:


这是未指定的,受到支持 N3751:对象生命周期,低级编程和 的memcpy 其中包括:

目前关于是否使用memcpy的C ++标准是无声的   复制对象表示字节在概念上是一个赋值或一个   对象构造。差异对于基于语义的问题很重要   程序分析和转换工具,以及优化器,   跟踪物体寿命。本文建议

  1. 使用memcpy复制两个不同的普通可复制表的两个不同对象的字节(但在其他方面具有相同的大小)   允许

  2. 这些用途被认为是初始化,或者更一般地被认为是(概念上)对象构造。

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

我找不到本文讨论的任何会议纪要,所以看起来它仍然是一个悬而未决的问题。

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

[...]对象由定义(3.1)通过new-expression创建   (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和obj2都不是   基类子对象,如果构成obj1的基础字节(1.7)是   复制到obj2,43 obj2随后应保持相同的值   OBJ1。[...]

这基本上就是提案所说的,所以这不应该是令人惊讶的。

dyp指出了关于这个话题的一个引人入胜的讨论 ub邮件列表[ub]输入双关语以避免复制

Propoal p0593:为低级对象操作隐式创建对象

提案p0593试图解决这个问题,但AFAIK还没有被审查。

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

它有一些激励性的例子,其性质相似,包括当前 的std ::矢量 当前具有未定义行为的实现。

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

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

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

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

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

  • 对memmove的调用就好像它一样

    • 将源存储复制到临时区域

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

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

    这允许memmove保留简单可复制对象的类型,或者用于将一个对象的字节表示重新解释为另一个对象的字节表示。

  • 对memcpy的调用与调用memmove的行为相同,只是它在源和目标之间引入了重叠限制。

  • 指定联合成员的类成员访问会在联合成员占用的存储中触发隐式对象创建。请注意,这不是一个全新的规则:对于成员访问位于赋值左侧但现在已作为此新框架的一部分进行推广的情况,此权限已存在于[P0137R1]中。如下所述,这不允许通过工会进行打字;相反,它只允许通过类成员访问表达式更改活动联合成员。

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

    // 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);
    

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

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


13
2018-05-08 02:41



也可以看看: open-std.org/pipermail/ub/2013-September/000127.html - dyp
不幸的是,据我所知,它是不完整的(开头是缺失的,结论是最好的模糊恕我直言)。 - dyp
我认为你的意思是“未指定”而不是“未指定”(后一术语在C ++标准中具有特定含义)? - M.M
另外,我有一个必然的问题(不确定是否值得将此作为一个单独的问题发布);如果你觉得这会有什么不同吗? T 有一个非平凡的默认构造函数? (但仍然可以轻易复制)。 - M.M
另一方面,“确实如此 memcpy 创建一个对象“问题似乎更多地受到通用操作的微不足道的可复制类型的驱动。例如,似乎”显而易见“当 std::vector 需要扩展和复制它的基础存储,包括简单的可复制 T 对象,它可以简单地分配一个更大的新的未初始化的存储,和 memcpy 现有的over对象(实际上标准明确保证两者之间的这种副本 T 对象是明确定义的)。但这是不允许的,因为没有T 对象尚未初始化存储。 - BeeOnRope


快速搜索

“...生命周期在分配正确对齐的对象存储空间时开始,并在存储空间被另一个对象释放或重用时结束。”

所以,我想通过这个定义,生命从分配开始,以免费结束。


3
2018-05-08 01:57



这样说似乎有点可疑 void *buf = malloc( sizeof(T) ) 创建了一个类型的对象 T。毕竟,它同样可以创建一个大小为的任何类型的对象 sizeof(T) ,我们还不知道这段代码是否会继续指出 T *b 在它,或 U *u 例如 - M.M
@nonsensickle我希望有一个“语言律师”的质量答案,例如来自C ++标准的文本支持malloc可以被认为是一个简单的构造函数 - M.M
@MattMcNabb,记忆来自 malloc 没有 声明的类型”。 stackoverflow.com/questions/31483064/...   就这样,它 有效的类型 在其一生中可以改变很多次;每次写入它都需要写入数据的类型。特别是,答案引用了如何 memcpy 复制源数据的有效类型。但我猜这是C,而不是C ++,也许它是不同的 - Aaron McDaid
@curiousguy:如果没有“有效类型”的概念,严格的别名规则将毫无意义。另一方面,我认为基于类型的别名规则本身的概念是一个错误,因为它同时迫使程序员使用低效的代码编写 memcpy 要么 memmove 并且希望优化器可以修复它,同时在程序员知道(并且可以告诉编译器)某些事情不会出现别名的情况下,无法允许编译器做出简单易用的优化。 - supercat
@curiousguy:我认为确实如此(这就是原因 char 得到特殊待遇)?虽然我承认我不理解什么是合法的规则而不是,因为规则是可怕的,与通过添加 __cache(x) {block} 声明,它将授权编译器假定其值 x 不会通过附加区块控制之外的任何方式进行更改。任何编译器只能通过拥有这样的语句来兼容 __cache(x) 是一个扩展为空的宏,但它将允许编译器做很多注册... - supercat


这段代码是否正确?

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

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

#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);
}

建成后 aa.x 被赋予一个值。让我们假设一下 std::string 未优化为小字符串值使用本地缓冲区,只是指向外部存储器块的数据指针。该 memcpy()复制内部数据 a as-is into buf。现在 a.x 和 b->x 请参考相同的内存地址 string 数据。什么时候 b->x 被赋予一个新值,该内存块被释放,但是 a.x 仍指它。什么时候 a 然后在结束时超出范围 main(),它试图再次释放相同的内存块。发生未定义的行为。

如果你想“正确”,将对象构造成现有内存块的正确方法是使用 投放新 运算符,例如:

#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);
}

0
2018-05-08 02:04



原始代码究竟是如何不正确的? - M.M
包含:: std :: string的struct T在c ++ 14及更高版本中不是简单的可复制的 - user1095108
包含一个对象的对象 std::string 从来没有被轻易复制过。它看起来像是一个复制+粘贴错误,问题中的代码有一个“平凡可复制”的注释,当代码被编辑为答案时,注释没有更新。 - Ben Voigt