在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
一个生命已经开始的物体? (如果是的话,它什么时候开始呢?)
这是未指定的,受到支持 N3751:对象生命周期,低级编程和
的memcpy 其中包括:
目前关于是否使用memcpy的C ++标准是无声的
复制对象表示字节在概念上是一个赋值或一个
对象构造。差异对于基于语义的问题很重要
程序分析和转换工具,以及优化器,
跟踪物体寿命。本文建议
使用memcpy复制两个不同的普通可复制表的两个不同对象的字节(但在其他方面具有相同的大小)
允许
这些用途被认为是初始化,或者更一般地被认为是(概念上)对象构造。
作为对象构造的识别将支持二进制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不足以触发隐式对象创建。
从 快速搜索。
“...生命周期在分配正确对齐的对象存储空间时开始,并在存储空间被另一个对象释放或重用时结束。”
所以,我想通过这个定义,生命从分配开始,以免费结束。
这段代码是否正确?
嗯,它通常会“起作用”,但仅适用于琐碎的类型。
我知道你没有要求它,但让我们使用一个非平凡类型的例子:
#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
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);
}