"constructing" 使用 memcpy 的简单可复制对象
"constructing" a trivially-copyable object with 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
是一个生命周期开始的对象吗? (如果有,具体是什么时候开始的?)
来自 a quick search。
"... lifetime begins when the properly-aligned storage for the object is allocated and ends when the storage is deallocated or reused by another object."
所以,根据这个定义,生命周期从分配开始,到免费结束。
Is this code correct?
嗯,通常会 "work",但仅适用于普通类型。
我知道你没有要求它,但让我们使用一个非平凡类型的例子:
#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()
结束时超出范围时,它会尝试再次释放同一个内存块。发生未定义的行为。
如果你想成为"correct",将对象构造到现有内存块中的正确方法是使用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);
}
N3751 支持的未指定:Object 生命周期,Low-level 编程,以及
memcpy 其中包括:
The C++ standards is currently silent on whether the use of memcpy to
copy object representation bytes is conceptually an assignment or an
object construction. The difference does matter for semantics-based
program analysis and transformation tools, as well as optimizers,
tracking object lifetime. This paper suggests that
uses of memcpy to copy the bytes of two distinct objects of two different trivial copyable tables (but otherwise of the same size) be
allowed
such uses are recognized as initialization, or more generally as (conceptually) object construction.
Recognition as object construction will support binary IO, while still
permitting lifetime-based analyses and optimizers.
我找不到任何讨论过这篇论文的会议纪要,所以它似乎仍然是一个悬而未决的问题。
C++14 草案标准目前在 1.8
[intro.object]:
[...]An object is created by a definition (3.1), by a new-expression
(5.3.4) or by the implementation (12.2) when needed.[...]
我们在 malloc
中没有,并且标准中涵盖的用于复制普通可复制类型的案例似乎仅指 3.9
部分中已经存在的 objects [basic.types]:
For any object (other than a base-class subobject) of trivially
copyable type T, whether or not the object holds a valid value of type
T, the underlying bytes (1.7) making up the object can be copied into
an array of char or unsigned char.42 If the content of the array of
char or unsigned char is copied back into the object, the object shall
subsequently hold its original value[...]
和:
For any trivially copyable type T, if two pointers to T point to
distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a
base-class subobject, if the underlying bytes (1.7) making up obj1 are
copied into obj2,43 obj2 shall subsequently hold the same value as
obj1.[...]
这基本上就是提案所说的内容,所以这不足为奇。
dyp 从 ub 邮件列表 中指出了关于此主题的精彩讨论:[ub] Type punning to avoid copying.
提案 p0593:为 low-level object 操纵隐式创建 objects
提案 p0593 试图解决这个问题,但 AFAIK 尚未经过审查。
This paper proposes that objects of sufficiently trivial types be created on-demand as necessary within newly-allocated storage to give programs defined behavior.
它有一些本质上相似的激励示例,包括当前具有未定义行为的 std::vector 实现。
它提出了以下隐式创建 object 的方法:
We propose that at minimum the following operations be specified as implicitly creating objects:
Creation of an array of char, unsigned char, or std::byte implicitly creates objects within that array.
A call to malloc, calloc, realloc, or any function named operator new or operator new[] implicitly creates objects in its returned storage.
std::allocator::allocate likewise implicitly creates objects in its returned storage; the allocator requirements should require other allocator implementations to do the same.
A call to memmove behaves as if it
copies the source storage to a temporary area
implicitly creates objects in the destination storage, and then
copies the temporary storage to the destination storage.
This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.
A call to memcpy behaves the same as a call to memmove except that it introduces an overlap restriction between the source and destination.
A class member access that nominates a union member triggers implicit object creation within the storage occupied by the union member. Note that this is not an entirely new rule: this permission already existed in [P0137R1] for cases where the member access is on the left side of an assignment, but is now generalized as part of this new framework. As explained below, this does not permit type punning through unions; rather, it merely permits the active union member to be changed by a class member access expression.
A new barrier operation (distinct from std::launder, which does not create objects) should be introduced to the standard library, with semantics equivalent to a memmove with the same source and destination storage. As a strawman, we suggest:
// 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);
In addition to the above, an implementation-defined set of non-stasndard memory allocation and mapping functions, such as mmap on POSIX systems and VirtualAlloc on Windows systems, should be specified as implicitly creating objects.
Note that a pointer reinterpret_cast is not considered sufficient to trigger implicit object creation.
代码现在是合法的,追溯自 C++98!
@Shafik Yaghmour 的回答很详尽,并且作为一个未解决的问题与代码有效性相关 - 回答时就是这种情况。 Shafik 的回答正确地引用了 p0593,在回答时它是一个提案。但从那以后,这个提议被接受了,事情也有了明确的定义。
一些历史
在 C++20 之前的 C++ 规范中没有提到使用 malloc
创建对象的可能性,例如参见 C++17 规范 [intro.object]:
The constructs in a C++ program create, destroy, refer to, access, and manipulate
objects. An object is created by a definition (6.1), by a new-expression (8.5.2.4),
when implicitly changing the active member of a union (12.3), or when a temporary
object is created (7.4, 15.2).
以上措辞并未提及 malloc
作为创建对象的选项,因此使其成为 de-facto 未定义行为。
它是 then viewed as a problem, and this issue was addressed later by https://wg21.link/P0593R6 并被接受为针对自 C++98 以来所有 C++ 版本的 DR,然后添加到 C++20 规范中,新措辞:
- The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below)...
...
- Further, after implicitly creating objects within a specified region of
storage, some operations are described as producing a pointer to a
suitable created object. These operations select one of the
implicitly-created objects whose address is the address of the start
of the region of storage, and produce a pointer value that points to
that object, if that value would result in the program having defined
behavior. If no such pointer value would give the program defined
behavior, the behavior of the program is undefined. If multiple such
pointer values would give the program defined behavior, it is
unspecified which such pointer value is produced.
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 已经解决了这个问题,这部分对 普通可复制类型 有效(措辞从 [=57= C++98 和 C++03 中的 ]POD 到 普通可复制类型 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);
}
请注意,为了完整起见,可以在释放 buf
之前添加对 *b
的析构函数的调用:
b->~T();
free(buf);
尽管 this is not required by the spec.
另外,删除 b 也是一个选项:
delete b;
// instead of:
// free(buf);
但如前所述,代码按原样有效。
在 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
是一个生命周期开始的对象吗? (如果有,具体是什么时候开始的?)
来自 a quick search。
"... lifetime begins when the properly-aligned storage for the object is allocated and ends when the storage is deallocated or reused by another object."
所以,根据这个定义,生命周期从分配开始,到免费结束。
Is this code correct?
嗯,通常会 "work",但仅适用于普通类型。
我知道你没有要求它,但让我们使用一个非平凡类型的例子:
#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()
结束时超出范围时,它会尝试再次释放同一个内存块。发生未定义的行为。
如果你想成为"correct",将对象构造到现有内存块中的正确方法是使用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);
}
N3751 支持的未指定:Object 生命周期,Low-level 编程,以及 memcpy 其中包括:
The C++ standards is currently silent on whether the use of memcpy to copy object representation bytes is conceptually an assignment or an object construction. The difference does matter for semantics-based program analysis and transformation tools, as well as optimizers, tracking object lifetime. This paper suggests that
uses of memcpy to copy the bytes of two distinct objects of two different trivial copyable tables (but otherwise of the same size) be allowed
such uses are recognized as initialization, or more generally as (conceptually) object construction.
Recognition as object construction will support binary IO, while still permitting lifetime-based analyses and optimizers.
我找不到任何讨论过这篇论文的会议纪要,所以它似乎仍然是一个悬而未决的问题。
C++14 草案标准目前在 1.8
[intro.object]:
[...]An object is created by a definition (3.1), by a new-expression (5.3.4) or by the implementation (12.2) when needed.[...]
我们在 malloc
中没有,并且标准中涵盖的用于复制普通可复制类型的案例似乎仅指 3.9
部分中已经存在的 objects [basic.types]:
For any object (other than a base-class subobject) of trivially copyable type T, whether or not the object holds a valid value of type T, the underlying bytes (1.7) making up the object can be copied into an array of char or unsigned char.42 If the content of the array of char or unsigned char is copied back into the object, the object shall subsequently hold its original value[...]
和:
For any trivially copyable type T, if two pointers to T point to distinct T objects obj1 and obj2, where neither obj1 nor obj2 is a base-class subobject, if the underlying bytes (1.7) making up obj1 are copied into obj2,43 obj2 shall subsequently hold the same value as obj1.[...]
这基本上就是提案所说的内容,所以这不足为奇。
dyp 从 ub 邮件列表 中指出了关于此主题的精彩讨论:[ub] Type punning to avoid copying.
提案 p0593:为 low-level object 操纵隐式创建 objects
提案 p0593 试图解决这个问题,但 AFAIK 尚未经过审查。
This paper proposes that objects of sufficiently trivial types be created on-demand as necessary within newly-allocated storage to give programs defined behavior.
它有一些本质上相似的激励示例,包括当前具有未定义行为的 std::vector 实现。
它提出了以下隐式创建 object 的方法:
We propose that at minimum the following operations be specified as implicitly creating objects:
Creation of an array of char, unsigned char, or std::byte implicitly creates objects within that array.
A call to malloc, calloc, realloc, or any function named operator new or operator new[] implicitly creates objects in its returned storage.
std::allocator::allocate likewise implicitly creates objects in its returned storage; the allocator requirements should require other allocator implementations to do the same.
A call to memmove behaves as if it
copies the source storage to a temporary area
implicitly creates objects in the destination storage, and then
copies the temporary storage to the destination storage.
This permits memmove to preserve the types of trivially-copyable objects, or to be used to reinterpret a byte representation of one object as that of another object.
A call to memcpy behaves the same as a call to memmove except that it introduces an overlap restriction between the source and destination.
A class member access that nominates a union member triggers implicit object creation within the storage occupied by the union member. Note that this is not an entirely new rule: this permission already existed in [P0137R1] for cases where the member access is on the left side of an assignment, but is now generalized as part of this new framework. As explained below, this does not permit type punning through unions; rather, it merely permits the active union member to be changed by a class member access expression.
A new barrier operation (distinct from std::launder, which does not create objects) should be introduced to the standard library, with semantics equivalent to a memmove with the same source and destination storage. As a strawman, we suggest:
// 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);
In addition to the above, an implementation-defined set of non-stasndard memory allocation and mapping functions, such as mmap on POSIX systems and VirtualAlloc on Windows systems, should be specified as implicitly creating objects.
Note that a pointer reinterpret_cast is not considered sufficient to trigger implicit object creation.
代码现在是合法的,追溯自 C++98!
@Shafik Yaghmour 的回答很详尽,并且作为一个未解决的问题与代码有效性相关 - 回答时就是这种情况。 Shafik 的回答正确地引用了 p0593,在回答时它是一个提案。但从那以后,这个提议被接受了,事情也有了明确的定义。
一些历史
在 C++20 之前的 C++ 规范中没有提到使用 malloc
创建对象的可能性,例如参见 C++17 规范 [intro.object]:
The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition (6.1), by a new-expression (8.5.2.4), when implicitly changing the active member of a union (12.3), or when a temporary object is created (7.4, 15.2).
以上措辞并未提及 malloc
作为创建对象的选项,因此使其成为 de-facto 未定义行为。
它是 then viewed as a problem, and this issue was addressed later by https://wg21.link/P0593R6 并被接受为针对自 C++98 以来所有 C++ 版本的 DR,然后添加到 C++20 规范中,新措辞:
- The constructs in a C++ program create, destroy, refer to, access, and manipulate objects. An object is created by a definition, by a new-expression, by an operation that implicitly creates objects (see below)...
...
- Further, after implicitly creating objects within a specified region of storage, some operations are described as producing a pointer to a suitable created object. These operations select one of the implicitly-created objects whose address is the address of the start of the region of storage, and produce a pointer value that points to that object, if that value would result in the program having defined behavior. If no such pointer value would give the program defined behavior, the behavior of the program is undefined. If multiple such pointer values would give the program defined behavior, it is unspecified which such pointer value is produced.
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 已经解决了这个问题,这部分对 普通可复制类型 有效(措辞从 [=57= C++98 和 C++03 中的 ]POD 到 普通可复制类型 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);
}
请注意,为了完整起见,可以在释放 buf
之前添加对 *b
的析构函数的调用:
b->~T();
free(buf);
尽管 this is not required by the spec.
另外,删除 b 也是一个选项:
delete b;
// instead of:
// free(buf);
但如前所述,代码按原样有效。