【问题标题】:How should I use placement new with a custom allocation API?我应该如何将placement new 与自定义分配API 一起使用?
【发布时间】:2017-03-21 06:01:10
【问题描述】:

我正在处理一些带有自定义分配和删除的内存空间,这些内存空间是使用类似 malloc 的接口制作的,这不受我的控制(即用于“分配 n 字节”或“释放分配的ptr”)。所以,没有像 newdelete 这样的东西。

现在,我想构造一个T 的数组。我得到了auto space_ptr = custom_alloc(n*sizeof(T)) 的空间。现在我想做一些类似 array-placement-new 的事情来就地构造 n 元素。我该怎么做? ...或者我应该从 1 循环到 n 并构造单个 T's?

注意

  • 我在这里忽略了对齐问题(或者更确切地说,假设alignof(T) 除以sizeof(T))。如果你想解决对齐问题,那就更好了,但为了简单起见,你可以忽略它。
  • 欢迎使用 C++11 代码(实际上是首选),但不使用 C++14/17。

【问题讨论】:

  • 有一个新的数组放置版本...但可能存在对齐问题。 this 是你需要的吗?
  • @AndyG:可能不会,因为放置new[] 使用一些空间来写入供delete[] 使用的信息。

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


【解决方案1】:

我会假设您的内存已与您的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&amp;... 构造 T 是否抛出。如果它抛出,并且-&gt;~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,即使 -&gt;~T() 是微不足道的,或者无数其他变体。)


正如 OP 所指出的,您可能还想在处理上述问题之前检查琐碎的构造。

【讨论】:

  • 我们有多大信心通过一个简单的构造函数为 T 优化掉它?另外,为什么要在销毁投掷时退出(-1)?
  • @einpoklum 因为那时我们已经在投掷了。建筑扔了。一个破坏者扔了出去。我们基本上完蛋了。我不相信它会被优化掉。我也会检查一下:但我会首先实现上述内容,并确认它确实会导致不必要的循环。至少,检测和跳过琐碎的位将使调试变得不那么荒谬。 :) 你会使用标签调度和std::is_trivial 或类似的东西。
  • 在分配的开头放置size_t 时要小心。分配本身通常是对齐的,但额外的size_t 可能会导致不对齐。我在现实世界的应用程序中遇到了这个问题,其中 clang 使用 SIMD 来初始化一个类中的两个双精度数,需要由 StdLib new/malloc 保证的完整 alignof(max_align_t),而 sizeof(size_t) &lt; alignof(max_align_t) 在这个典型的x64 设置。
【解决方案2】:

实际上,您可以将分配逻辑与匹配的解除分配逻辑“插入”到内置的新表达式中。这可以通过自定义 operator new 和 operator delete 来完成。放置新表达式实际上采用任意数量的放置参数;这些参数用于查找重载的运算符 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 %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

【讨论】:

    猜你喜欢
    • 2023-03-22
    • 1970-01-01
    • 2021-08-22
    • 1970-01-01
    • 2013-08-04
    • 1970-01-01
    • 2016-07-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多