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

时间:2015-05-08 01:35:50

标签: c++ language-lawyer lifetime

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

4 个答案:

答案 0 :(得分:13)

这是未指定的,由N3751: Object Lifetime, Low-level Programming, and memcpy支持,其中包括:

  

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

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

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

  4.         

    识别为对象构造将支持二进制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] Type punning to avoid copying

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的行为相同,只是它在源和目标之间引入了重叠限制。

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

  •   
  • 应该在标准库中引入一个新的屏障操作(不同于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不足以触发隐式对象创建。

答案 1 :(得分:3)

来自a quick search

  

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

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

答案 2 :(得分:2)

该代码现已合法,并且可追溯到C ++ 98!

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

一些历史

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

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

以上措词并未将malloc称为创建对象的选项,因此使它成为事实上的未定义行为。

它是then viewed as a problem,后来https://wg21.link/P0593R6解决了这个问题,并接受了自C ++ 98起(包括C ++ 98)的所有C ++版本的DR,然后添加到C ++ 20规范中,新的措辞:

[intro.object]

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

...

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

C ++ 20规范中给出的example是:

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

关于memcpy的使用-@Shafik Yaghmour已经解决了这一点,该部分适用于普通可复制类型(措辞从 POD 中的C ++ 98和C ++ 03到简单可复制的类型 in 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);
} 

请注意,出于完整性考虑,在释放*b之前,可能会向buf的析构函数添加调用:

b->~T();
free(buf);

尽管this is not required by the spec

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

delete b;
// instead of:
// free(buf);

但是,正如所说的那样,代码仍然有效。

答案 3 :(得分:1)

  

这段代码是否正确?

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

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

#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.xb->x引用string数据的相同内存地址。为b->x分配新值时,将释放该内存块,但a.x仍然引用它。当amain()结束时超出范围时,它会再次尝试释放相同的内存块。发生未定义的行为。

如果您想要“正确”,将对象构建到现有内存块的正确方法是使用 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);
}