我应该如何使用放置新的自定义分配API

How should I use placement new with a custom allocation API?

本文关键字:自定义 分配 API 何使用 我应该      更新时间:2023-10-16

我正在处理一些具有自定义分配和删除的内存空间,这是使用类似malloc的接口制作的,这不在我的控制之下(即不透明的c风格函数用于"分配n个字节");或者"释放已分配的资源"。所以,不像newdelete

现在,我想构造一个T的数组,我用auto space_ptr = custom_alloc(n*sizeof(T))来得到它的空间。现在我想做一些类似于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是否抛出。如果它抛出,并且->~T()是非平凡的,则使用异常安全的。

c++ 17公开了一些新函数来完成这些任务。他们可能会处理极端情况,我的就不会。


如果你试图模拟new[]delete[],如果T有一个非平凡的医生,你必须嵌入多少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所指出的,在进行上述操作之前,您可能还需要检查是否存在琐碎的构造。

实际上,您可以将分配逻辑以及匹配的释放逻辑"插件"到内置的new表达式中。这可以通过自定义操作符new和操作符delete来实现。位置new表达式实际上接受任意数量的位置参数;这些实参用于查找重载操作符new和重载操作符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 %lun", size);
    return static_cast<T*>(::operator new(size));
  }
  void deallocate(T* p, size_t)
  {
    printf("deallocaten");
    return ::operator delete(p);
  }
};
struct fail_halfway {
  static size_t counter;
  size_t idx;
  fail_halfway()
    : idx(++counter)
  {
    printf("I am %lun", idx);
    if (idx == 5)
      throw 42;
  }
  ~fail_halfway()
  {
    printf("%lu dyingn", 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