用memcpy"构造"一个​​可复制的对象

M.M*_*M.M 11 c++ lifetime language-lawyer

在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);
}
Run Code Online (Sandbox Code Playgroud)

换句话说,是*b一个生命已经开始的对象?(如果是的话,它什么时候开始呢?)

Sha*_*our 13

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

C++标准目前没有说明使用memcpy来复制对象表示字节在概念上是分配还是对象构造.差异对于基于语义的程序分析和转换工具以及优化器,跟踪对象生命周期至关重要.本文建议

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

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

作为对象构造的识别将支持二进制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和obj2都不是基类子对象,如果构成obj1的基础字节(1.7)被复制到obj2,43 obj2中随后应与obj1保持相同的价值.[...]

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

dyp从ub邮件列表中指出了关于这个主题的一个引人入胜的讨论:[ub]键入punning以避免复制.

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

提案p0593试图解决这个问题,但AFAIK尚未得到审查.

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

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

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

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

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

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

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

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

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

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

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

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

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

  • 提名联盟成员的类成员访问会触发由union成员占用的存储中的隐式对象创建.请注意,这不是一个全新的规则:对于成员访问位于赋值左侧的情况,此权限已存在于[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);
    
    Run Code Online (Sandbox Code Playgroud)

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

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


Ami*_*rsh 5

代码现在是合法的,并且从 C++98 开始追溯!

@Shafik Yaghmour 的回答是彻底的,并且与作为一个开放问题的代码有效性相关 - 回答时就是这种情况。Shafik 的回答正确地参考了 p0593,在回答时它是一个提案。但从那以后,该提案被接受了,事情也得到了明确。

一些历史

malloc在 C++20 之前的 C++ 规范中没有提到使用创建对象的可能性,例如参见 C++17 规范[intro.object]

C++ 程序中的构造创建、销毁、引用、访问和操作对象。对象由定义 (6.1)、new 表达式 (8.5.2.4)、隐式更改联合的活动成员 (12.3) 或创建临时对象 (7.4, 15.2) 创建。

以上措辞并不是malloc创建对象的选项,因此使其成为事实上的未定义行为。

然后它被视为一个问题,这个问题后来由https://wg21.link/P0593R6解决,并被接受为自 C++98 以来所有 C++ 版本的 DR,然后添加到 C++20 规范中,新的措辞:

[介绍对象]

  1. C++ 程序中的构造创建、销毁、引用、访问和操作对象。一个对象是由一个定义、一个新表达式、一个隐式创建对象的操作创建的(见下文) ......

...

  1. 此外,在指定的存储区域内隐式创建对象之后,一些操作被描述为生成指向合适的创建对象的指针。这些操作选择一个隐式创建的对象,其地址是存储区域的起始地址,并产生一个指向该对象的指针值,如果该值将导致程序具有定义的行为。如果没有这样的指针值会给程序定义的行为,则程序的行为是未定义的。如果多个这样的指针值会给程序定义的行为,则未指定产生哪个这样的指针值。

C++20 规范中给出的示例是:

#include <cstdlib>
struct X { int a, b; };
X *make_x() {
   // The call to std?::?malloc implicitly creates an object of type X
   // and its subobjects a and b, and returns a pointer to that X object
   // (or an object that is pointer-interconvertible ([basic.compound]) with it), 
   // in order to give the subsequent class member access operations   
   // defined behavior. 
   X *p = (X*)std::malloc(sizeof(struct X));
   p->a = 1;   
   p->b = 2;
   return p;
}
Run Code Online (Sandbox Code Playgroud)

至于使用memcpy- @Shafik Yaghmour 已经解决了这一点,这部分对于可简单复制的类型有效(措辞从C++98 和 C++03 中的POD更改为C++11及之后的普通可复制类型 )。


底线:代码有效。

至于生命周期的问题,让我们深入研究一下有问题的代码:

struct T   // trivially copyable type
{
    int x, y;
};

int main()
{
    void *buf = std::malloc( sizeof(T) ); // <= just an allocation
    if ( !buf ) return 0;

    T a{}; // <= here an object is born of course
    std::memcpy(buf, &a, sizeof a);      // <= just a copy of bytes
    T *b = static_cast<T *>(buf);        // <= here an object is "born"
                                         //    without constructor    
    b->x = b->y;

    free(buf);
} 
Run Code Online (Sandbox Code Playgroud)

请注意,*b为了完整起见,可以在释放 之前添加对 的析构函数的调用buf

b->~T();
free(buf);
Run Code Online (Sandbox Code Playgroud)

虽然这不是规范所要求的

或者,删除 b也是一种选择:

delete b;
// instead of:
// free(buf);
Run Code Online (Sandbox Code Playgroud)

但如前所述,代码按原样有效。


归档时间:

查看次数:

1377 次

最近记录:

7 年,3 月 前