Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create an array while potentially using placement new

I have been working on creating a custom allocator as a fun exercise/practice and I ran into two potentials issues with creating arrays. For a typical call for allocation, I will use malloc and placement new. However, when I go to create an array I am confused on how it should be done. For once, I have noticed at some places that it seems placement new may not be safe for arrays such as here. I am also running into an error of my own while attempting to use placement new for an array. I will get the error of

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

I understand the error (I believe) but I would prefer to have the error be solved via my array construction method. I have two questions

1) How can an allocator create an array without using new[]? Is it with placement new? If so, what about the potential dangers mentioned from the link I posted above?

2) If I am suppose to use placement new and call it on each one of the elements in the array, why am I getting the error mentioned above?

#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);
}
like image 301
mmurphy Avatar asked Nov 05 '22 01:11

mmurphy


2 Answers

1) How can an allocator create an array without using new[]? Is it with placement new? If so, what about the potential dangers mentioned from the link I posted above?

The problem in the link is misunderstanding how things work. Each implementation has an implementation defined means to record information about an allocated array. This information is not required with single objects, because it is managed by the client via the implementation of delete.

With an array, the implementation must record things such as element count, destructor to call (if applicable), element size… this stuff is often stored at the start of the returned allocation, and the implementation obviously offsets the size of the allocation request appropriately. Thus, the actual size is offset in order to accommodate these hidden values. This is why malloc(sizeof...) will not work unless your allocator does additional bookkeeping (which, btw, std::allocator and collections interfaces bring).

To record this information correctly, you could define static void* operator new[]. To incorporate your own allocator in this scheme via placement, you could use the following approach:

// 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;
}

note that you would similarly implement placement operator delete[] if your allocator may throw.

if you want your allocator to do some bookkeeping, it gets messy. personally, i don't think this situation is implemented well by the language, especially since array initialization was not implemented well. there will always be an additional step to perform near construction or destruction, or some globally accessible data to use in this context.

2) If I am suppose to use placement new and call it on each one of the elements in the array, why am I getting the error mentioned above?

you'd need to explicitly construct elements if you're creating an allocator which does not go through operator new/operator new[]. extending the above example, you would want a destroy method which called delete[], then told this to free/reuse the memory (rather than the use of free above).

if you just want a quick solution, you wll need to lug the destructor, size, and element count around with the allocation or allocator. in that scenarion, you do not use new[]/delete[].

Edit

and if you want to manage the books yourself, here's one approach (which could go many directions):

#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);
  }
};

Demo:

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;
}
like image 164
justin Avatar answered Nov 09 '22 10:11

justin


mem[i] has type Type& while new(mem + i) Type(); has type Type*. These are clearly incompatible types and can't be assigned. I believe you can remove the assignment completely and it will work out, still initializing the memory at that location for you.

I'd still be slightly wary of implementing your own array allocators though (a custom allocator for vector would be more obvious for example).

like image 23
Mark B Avatar answered Nov 09 '22 10:11

Mark B