如何使用placement new创建数组

5
我一直在尝试创建一个自定义分配器作为有趣的练习,但在创建数组时遇到了两个潜在问题。对于典型的分配调用,我将使用mallocplacement new。然而,当我要创建一个数组时,我不知道该怎么做。首先,我注意到在某些地方,似乎像这里那样,placement new可能对数组不安全。我也遇到了自己的错误,当我尝试对数组使用placement new时,我会得到以下错误:

`error C2679: binary '=' : no operator found which takes a right-hand operand of type 'SomeClass *' (or there is no acceptable conversion)

我理解了这个错误(我相信),但我更喜欢通过我的数组构造方法来解决这个错误。我有两个问题:
1)分配器如何在不使用new[]的情况下创建数组?是使用placement new吗?如果是,那么上面提到的潜在危险怎么办?
2)如果我应该使用placement new并在数组中调用它的每个元素,为什么我会得到上述错误?
#include <stdio.h>
#include <new>

class SomeClass{
public:
    SomeClass() {
        printf("Constructed\n");
    }

    ~SomeClass() {
        printf("Destructed\n");
    }
};

void* SomeAllocationFunction(size_t size) {
    return malloc(size);
}

template<typename Type>
Type* SomeArrayAllocationFunction(size_t count){
    Type* mem = (Type*)SomeAllocationFunction(sizeof(Type) * count);

    for(unsigned int i = 0; i < count; ++i)
    {
        mem[i] = new(mem + i) Type();
    }

    return mem; 
}

int main(void){
    SomeClass* t = SomeArrayAllocationFunction<SomeClass>(2);
}
2个回答

1

mem[i] 的类型是 Type&,而 new(mem + i) Type(); 的类型是 Type*。这些类型明显不兼容,不能被赋值。我相信你可以完全删除赋值,它仍然会为你在该位置初始化内存。

尽管如此,我仍然有点担心实现自己的数组分配器(例如,为vector编写自定义分配器会更明显)。


啊,是的,new(mem + i) Type(); 似乎就是所需的全部了。然而,使用 placement new 来分配数组是否危险呢?我发现有些地方认为它是危险的。 - mmurphy
@mmurphy 如果你知道你在做什么,它并不危险。只是在你的情况下不需要这样做。 - RedX
@mmurphy,正如Mark所说。使用自定义的allocator<T>向量将是一个更明显的解决方案。 - RedX
@mmurphy我以前没有看到过针对数组的这种做法,因为vector已经提供了这个解决方案。 - Mark B
@MarkB:分配器是如何创建数组的呢? - mmurphy
显示剩余2条评论

1
1)分配器如何在不使用new[]的情况下创建数组?是用放置new吗?如果是这样,那么上面链接中提到的潜在危险怎么办?
链接中的问题是对事物运作方式的误解。每个实现都有一种实现定义的方式来记录有关已分配数组的信息。由于这些信息由客户端通过delete的实现进行管理,因此单个对象不需要这些信息。
对于数组,实现必须记录诸如元素计数、要调用的析构函数(如果适用)、元素大小等内容,这些通常存储在返回的分配的开始处,实现显然会相应地偏移分配请求的大小。因此,实际大小被抵消以容纳这些隐藏值。这就是为什么malloc(sizeof...)将不起作用,除非您的分配器进行了额外的簿记(btw,std::allocator和集合接口也带有)。
为了正确记录这些信息,您可以定义静态void* operator new[]。要通过放置在此方案中结合自己的分配器,可以使用以下方法:
// quick/dirty/incomplete illustration:
#include <stdio.h>
#include <new>
#include <cstdlib>

class t_allocator {
public:
    t_allocator() {
    }

    ~t_allocator() {
    }

public:
    void* allocate(const size_t& size) {
        return malloc(size);
    }
};

class SomeClass {
public:
    SomeClass() {
        printf("Constructed\n");
    }

    ~SomeClass() {
        printf("Destructed\n");
    }

public:
    static void* operator new[](size_t size, t_allocator& allocator) {
        return allocator.allocate(size);
    }

    /* in case static void* operator new[](size_t size, t_allocator& allocator) throws: */
    static void operator delete[](void* object, t_allocator& allocator) {
        /* ... */
    }

    static void operator delete[](void* object) {
        /* matches t_allocator::allocate */
        free(object);
    }
};

int main(void) {
    t_allocator allocator;
    SomeClass* t(new (allocator) SomeClass[2]);

    delete[] t;
    t = 0;

    return 0;
}

请注意,如果您的分配器可能会抛出异常,同样可以实现放置operator delete[]
如果您希望您的分配器进行一些簿记记录,那么情况就会变得混乱。个人认为,语言没有很好地实现这种情况,特别是由于数组初始化没有很好地实现。在构建或销毁附近始终需要执行额外的步骤,或者在此上下文中使用一些全局可访问的数据。

2) 如果我应该使用放置new并在数组的每个元素上调用它,为什么会出现上述错误?

如果您正在创建一个不通过operator new/operator new[]的分配器,则需要显式构造元素。扩展上面的示例,您将需要一个destroy方法,该方法调用delete[],然后告诉this释放/重用内存(而不是上面的free)。
如果您只想要一个快速解决方案,您需要在分配或分配器中携带析构函数、大小和元素计数。在这种情况下,您不使用new[]/delete[]编辑

如果你想自己管理这些书,这里有一个方法(可以有很多方向):

#include <cassert>
#include <stdio.h>
#include <new>
#include <cstdlib>

class t_allocator {
public:
  t_allocator() {
  }

  ~t_allocator() {
  }

public:
  /** tracks an array allocation's data. acts as a scope container for the allocation/types. */
  class t_array_record {
  public:
    typedef void (*t_destructor)(void* const);

    template<typename T>
    t_array_record(T*& outObjects, t_allocator& allocator, const size_t& count) : d_mem(allocator.allocate(sizeof(T), count)), d_destructor(t_allocator::t_array_record::Destruct<T>), d_size(sizeof(T)), d_count(count), d_allocator(allocator) {
      assert(this->d_mem);
      /* mind exceptions */
      char* const cptr(reinterpret_cast<char*>(this->d_mem));

      for (size_t idx(0); idx < this->d_count; ++idx) {
        /* assignment not required here: */
        new (&cptr[this->d_size * idx]) T();
      }

      outObjects = reinterpret_cast<T*>(this->d_mem);
    }

    ~t_array_record() {
      assert(this->d_mem);
      char* const cptr(reinterpret_cast<char*>(this->d_mem));

      for (size_t idx(0); idx < this->d_count; ++idx) {
        const size_t element(this->d_count - idx - 1U);
        this->d_destructor(& cptr[this->d_size * element]);
      }

      this->d_allocator.free(this->d_mem);
    }

  private:
    template<typename T>
    static void Destruct(void* const ptr) {
      T* const obj(reinterpret_cast<T*>(ptr));

      obj->~T();
    }

  private:
    void* const d_mem;
    t_destructor d_destructor;
    const size_t d_size;
    const size_t d_count;
    t_allocator& d_allocator;
  public:
    t_array_record(const t_array_record&);
    t_array_record& operator=(const t_array_record&);
  };
public:
  void* allocate(const size_t& size, const size_t& count) {
    return malloc(size * count);
  }

  void free(void* const mem) {
    ::free(mem);
  }
};

演示:

class SomeClass {
public:
  SomeClass() {
    printf("Constructed\n");
  }

  virtual ~SomeClass() {
    printf("Destructed\n");
  }

  virtual void greet() {
    printf("hi: %p\n", this);
  }

private:
  SomeClass(const SomeClass&);
  SomeClass& operator=(const SomeClass&);
};

class SomeDer : public SomeClass {
  static int& N() {
    static int a(0);

    return ++a;
  }

public:
  SomeDer() : d_number(N()) {
    printf("Ctor-%i\n", this->d_number);
  }

  virtual ~SomeDer() {
    printf("~Der%i-", this->d_number);
  }

  virtual void greet() {
    printf("Der%i-", this->d_number);
    SomeClass::greet();
  }

private:
  const int d_number; /* << so we have different sized types in the example */
};

template<typename T>
void TryIt(const size_t& count) {
  t_allocator allocator;

  T* things(0);
  t_allocator::t_array_record record(things, allocator, count);

  for (size_t idx(0); idx < count; ++idx) {
    things[idx].greet();
  }
}

int main() {
  TryIt<SomeClass>(3);
  TryIt<SomeDer>(9);
  return 0;
}

你如何在不过载类的 new[] 和 delete[] 的情况下完成你的示例?我希望不必为每个类都重载 new 和 delete,而是使用一个分配器。 - mmurphy
@mmurphy 如果您愿意,您可以将它们定义为自由运算符。另一种方法已经发送到您的邮箱中... - justin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接