我应该如何使用带有自定义分配 API 的新展示位置?
How should I use placement new with a custom allocation API?
我正在处理一些具有自定义分配和删除功能的内存 space,这些内存是使用类似 malloc 的界面创建的,不受我的控制(即不透明的 C 风格函数,用于“分配 n字节”或“释放分配的指针”)。所以,不像 new
或 delete
.
现在,我想构造一个 T
的数组。我用 auto space_ptr = custom_alloc(n*sizeof(T))
得到了 space。现在我想做一些类似 array-placement-new 的事情来就地构建 n
元素。我该怎么做? ...或者我应该从 1 循环到 n
并构造单个 T
?
注:
- 我在这里忽略对齐问题(或者更确切地说,假设
alignof(T)
除 sizeof(T)
)。如果你想解决对齐问题,那会更好,但为了简单起见,你可以忽略它。
- 欢迎使用 C++11 代码(实际上是首选),但不要使用 C++14/17。
我假设您的记忆力足以满足您的 T
。你可能想检查一下。
下一个问题是异常。真的应该写两个版本,一个有可能构造出异常,一个没有。
我会写异常安全版本。
template<class T, class...Args>
T* construct_n_exception_safe( std::size_t n, void* here, Args&&...args ) {
auto ptr = [here](std::size_t i)->void*{
return static_cast<T*>(here)+i;
};
for( std::size_t i = 0; i < n; ++i ) {
try {
new(ptr(i)) T(args...);
} catch( ... ) {
try {
for (auto j = i; j > 0; --j) {
ptr(j-1)->~T();
}
} catch ( ... ) {
exit(-1);
}
throw;
}
}
return static_cast<T*>(here);
}
和非例外安全版本:
template<class T, class...Args>
T* construct_n_not_exception_safe( std::size_t n, void* here, Args&&...args ) {
auto ptr = [here](std::size_t i)->void*{
return static_cast<T*>(here)+i;
};
for(std::size_t i = 0; i < n; ++i) {
new (ptr(i)) T(args...);
}
return static_cast<T*>(here);
}
你可以做一个基于标签分配的系统来根据是否从 Args&...
构造 T
throws 来在它们之间进行选择。如果它抛出,并且 ->~T()
是不平凡的,请使用异常安全的。
C++17 公开了一些新函数来完成这些任务。他们可能会处理我不会处理的极端情况。
如果您尝试模拟 new[]
和 delete[]
,如果 T
有一个非平凡的 dtor,您将必须嵌入您创建的 T
数量在区块中。
执行此操作的典型方法是在块的前面 处为计数请求额外的空间。即,要求 sizeof(T)*N+K
,其中 K
可能是 sizeof(std::size_t)
.
现在,在您的 new[]
模拟器中,将 N
填充到第一位,然后在紧跟其后的块上调用 construct_n
。
在delete[]
中,传入的指针减去sizeof(std::size_t)
,读取N
然后销毁对象(从右到左镜像构建顺序)
这一切都需要小心try
-catch
.
但是,如果 ~T()
是微不足道的,那么您模拟的 new[]
和 delete[]
都不会存储额外的 std::size_t
也不会读取它。
(请注意,这是 模拟 new[]
和 delete[]
的方法。new[]
和 delete[]
的工作原理是依赖于实现。我只是勾勒出一种你可以模拟它们的方法,它可能与它们在你的系统上的工作方式不兼容。例如,一些 ABI 可能总是存储 N
,即使 ->~T()
是琐碎的,或无数其他变体。)
正如 OP 所指出的,您可能还想在进行上述操作之前检查琐碎的构造。
实际上,您可以 "plug-in" 将分配逻辑连同匹配的释放逻辑一起用于内置的 new 表达式。这可以通过自定义 operator new 和 operator delete 来完成。放置新表达式实际上采用任意数量的放置参数;这些参数用于查找重载的 operator new,以及重载的 operator delete(如果有的话)。 New 表达式将调用该运算符 new 来分配内存并构造对象。如果数组构造中途抛出异常,编译器会帮你销毁那些完成的对象,最后调用匹配的操作符delete。
使用类似接口的 STL 分配器的示例代码:
#include <cstdio>
#include <memory>
// tag type to select our overloads
struct use_allocator_t {
explicit use_allocator_t() = default;
};
// single-object forms skipped, just the same thing without []
template <class A>
void* operator new[](size_t size, use_allocator_t, A a)
{
using traits = std::allocator_traits<A>;
return traits::allocate(a, size);
}
template <class A>
void operator delete[](void* p, use_allocator_t, A a)
{
using traits = std::allocator_traits<A>;
return traits::deallocate(a, static_cast<typename traits::pointer>(p), 0);
}
template <class T>
struct barfing_allocator {
using value_type = T;
T* allocate(size_t size)
{
printf("allocate %lu\n", size);
return static_cast<T*>(::operator new(size));
}
void deallocate(T* p, size_t)
{
printf("deallocate\n");
return ::operator delete(p);
}
};
struct fail_halfway {
static size_t counter;
size_t idx;
fail_halfway()
: idx(++counter)
{
printf("I am %lu\n", idx);
if (idx == 5)
throw 42;
}
~fail_halfway()
{
printf("%lu dying\n", idx);
}
};
size_t fail_halfway::counter = 0;
int main()
{
barfing_allocator<fail_halfway> a;
try {
new (use_allocator_t(), a) fail_halfway[10];
} catch(int) {
return 0;
}
return 1;
}
代码将打印:
allocate 88
I am 1
I am 2
I am 3
I am 4
I am 5
4 dying
3 dying
2 dying
1 dying
deallocate
我正在处理一些具有自定义分配和删除功能的内存 space,这些内存是使用类似 malloc 的界面创建的,不受我的控制(即不透明的 C 风格函数,用于“分配 n字节”或“释放分配的指针”)。所以,不像 new
或 delete
.
现在,我想构造一个 T
的数组。我用 auto space_ptr = custom_alloc(n*sizeof(T))
得到了 space。现在我想做一些类似 array-placement-new 的事情来就地构建 n
元素。我该怎么做? ...或者我应该从 1 循环到 n
并构造单个 T
?
注:
- 我在这里忽略对齐问题(或者更确切地说,假设
alignof(T)
除sizeof(T)
)。如果你想解决对齐问题,那会更好,但为了简单起见,你可以忽略它。 - 欢迎使用 C++11 代码(实际上是首选),但不要使用 C++14/17。
我假设您的记忆力足以满足您的 T
。你可能想检查一下。
下一个问题是异常。真的应该写两个版本,一个有可能构造出异常,一个没有。
我会写异常安全版本。
template<class T, class...Args>
T* construct_n_exception_safe( std::size_t n, void* here, Args&&...args ) {
auto ptr = [here](std::size_t i)->void*{
return static_cast<T*>(here)+i;
};
for( std::size_t i = 0; i < n; ++i ) {
try {
new(ptr(i)) T(args...);
} catch( ... ) {
try {
for (auto j = i; j > 0; --j) {
ptr(j-1)->~T();
}
} catch ( ... ) {
exit(-1);
}
throw;
}
}
return static_cast<T*>(here);
}
和非例外安全版本:
template<class T, class...Args>
T* construct_n_not_exception_safe( std::size_t n, void* here, Args&&...args ) {
auto ptr = [here](std::size_t i)->void*{
return static_cast<T*>(here)+i;
};
for(std::size_t i = 0; i < n; ++i) {
new (ptr(i)) T(args...);
}
return static_cast<T*>(here);
}
你可以做一个基于标签分配的系统来根据是否从 Args&...
构造 T
throws 来在它们之间进行选择。如果它抛出,并且 ->~T()
是不平凡的,请使用异常安全的。
C++17 公开了一些新函数来完成这些任务。他们可能会处理我不会处理的极端情况。
如果您尝试模拟 new[]
和 delete[]
,如果 T
有一个非平凡的 dtor,您将必须嵌入您创建的 T
数量在区块中。
执行此操作的典型方法是在块的前面 处为计数请求额外的空间。即,要求 sizeof(T)*N+K
,其中 K
可能是 sizeof(std::size_t)
.
现在,在您的 new[]
模拟器中,将 N
填充到第一位,然后在紧跟其后的块上调用 construct_n
。
在delete[]
中,传入的指针减去sizeof(std::size_t)
,读取N
然后销毁对象(从右到左镜像构建顺序)
这一切都需要小心try
-catch
.
但是,如果 ~T()
是微不足道的,那么您模拟的 new[]
和 delete[]
都不会存储额外的 std::size_t
也不会读取它。
(请注意,这是 模拟 new[]
和 delete[]
的方法。new[]
和 delete[]
的工作原理是依赖于实现。我只是勾勒出一种你可以模拟它们的方法,它可能与它们在你的系统上的工作方式不兼容。例如,一些 ABI 可能总是存储 N
,即使 ->~T()
是琐碎的,或无数其他变体。)
正如 OP 所指出的,您可能还想在进行上述操作之前检查琐碎的构造。
实际上,您可以 "plug-in" 将分配逻辑连同匹配的释放逻辑一起用于内置的 new 表达式。这可以通过自定义 operator new 和 operator delete 来完成。放置新表达式实际上采用任意数量的放置参数;这些参数用于查找重载的 operator new,以及重载的 operator delete(如果有的话)。 New 表达式将调用该运算符 new 来分配内存并构造对象。如果数组构造中途抛出异常,编译器会帮你销毁那些完成的对象,最后调用匹配的操作符delete。
使用类似接口的 STL 分配器的示例代码:
#include <cstdio>
#include <memory>
// tag type to select our overloads
struct use_allocator_t {
explicit use_allocator_t() = default;
};
// single-object forms skipped, just the same thing without []
template <class A>
void* operator new[](size_t size, use_allocator_t, A a)
{
using traits = std::allocator_traits<A>;
return traits::allocate(a, size);
}
template <class A>
void operator delete[](void* p, use_allocator_t, A a)
{
using traits = std::allocator_traits<A>;
return traits::deallocate(a, static_cast<typename traits::pointer>(p), 0);
}
template <class T>
struct barfing_allocator {
using value_type = T;
T* allocate(size_t size)
{
printf("allocate %lu\n", size);
return static_cast<T*>(::operator new(size));
}
void deallocate(T* p, size_t)
{
printf("deallocate\n");
return ::operator delete(p);
}
};
struct fail_halfway {
static size_t counter;
size_t idx;
fail_halfway()
: idx(++counter)
{
printf("I am %lu\n", idx);
if (idx == 5)
throw 42;
}
~fail_halfway()
{
printf("%lu dying\n", idx);
}
};
size_t fail_halfway::counter = 0;
int main()
{
barfing_allocator<fail_halfway> a;
try {
new (use_allocator_t(), a) fail_halfway[10];
} catch(int) {
return 0;
}
return 1;
}
代码将打印:
allocate 88
I am 1
I am 2
I am 3
I am 4
I am 5
4 dying
3 dying
2 dying
1 dying
deallocate