我应该如何在自定义分配API中使用placement new?

时间:2016-11-07 19:16:55

标签: c++ c++11 memory-management placement-new

我正在使用自定义分配和删除工作一些内存空间,这是使用我无法控制的类似malloc的接口(即分配n个字节或释放分配的ptr)。所以,不像delete[]

现在,我想构建一个T&#39的数组。我用auto space_ptr = magic_malloc(n*sizeof(T))得到了空间。现在我想做一些像array-placement-new这样的内容来构建n个元素。我该怎么办? ...或者我应该从1循环到n并构建单个T'

注意

  • 我忽略了对齐问题(或者更确切地说,假设alignof(T)除以sizeof(T))。如果你想解决对齐问题,那就更好了,但为了简单起见,你可能会忽略它。
  • C ++ 11 code welcome(实际上是首选),但没有C ++ 14/17。

2 个答案:

答案 0 :(得分:5)

我会假设你的记忆与你的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);
}

您可以执行基于标记调度的系统,以便在它们之间进行选择,具体取决于是否从T构建Args&...。如果它抛出,->~T()非常重要,请使用异常安全的。

C ++ 17公开了一些新函数来完成这些任务。他们可能会处理我所不认识的角落案件。

如果您要模仿new[]delete[],如果T有一个非常重要的内容,那么您必须嵌入您创建的Tsizeof(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[]N的确切工作方式取决于实现.I&# 39;我只是勾勒出一种可以模仿它们的方式,它可能与它们在你的系统上的工作方式不兼容。例如,即使->~T()是微不足道的,一些ABI也可能总是存储{{1}},或者无数其他变种。)

正如OP所指出的那样,在打扰上述内容之前,您可能还需要检查琐碎的构造。

答案 1 :(得分:0)

实际上,您可以将分配逻辑以及匹配的解除分配逻辑“插入”到内置的新表达式中。可以使用自定义运算符new和运算符delete来完成。实际上,placement new表达式采用任意数量的placement参数。这些参数用于查找重载的运算符new,也可以查找重载的运算符delete(如果有)。新的表达式将调用该运算符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