6

Currently I am working on custom memory allocation and one of the drawbacks is that I have to write multiple lines to achieve the same result that the new-expression provides with just one simple call.

Simple initialization:

MyClass *obj = new MyClass(3.14);

Less simple initialization:

void *obj_mem = alloc->Allocate(sizeof MyClass, alignof(MyClass));
MyClass *obj = new(obj_mem) MyClass(3.14);

I am going to provide my project group with allocators such as that one, and want them to actually use them, instead of falling back on calling new, since we'll need these faster allocators to manage our memory.

But for that to happen, I will have to devise the simplest possible syntax to initialize a variable with my custom allocators.


My Solution

My best bet was overriding operator new in each class, since it is the allocation function for the new-expression.

class MyClass
{
    ...

    void* operator new(size_t size, Allocator *alloc)
    {
        return alloc->Allocate(size, alignof(MyClass));
    }
}

And then the syntax to initialize a variable becomes what I ultimately want:

MyClass *obj = new(alloc) MyClass(3.14);

However, it would be great if I could have a general equivalent of the above. So I wouldn't have to override operator new for each class.

Community
  • 1
  • 1
Nikita
  • 609
  • 2
  • 7
  • 18
  • 1
    This sounds like an [XY problem](http://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). If a C++ class needs to allocate memory when instantiated. that's typically done in the class constructor. – MrEricSir Dec 04 '16 at 05:16
  • 2
    `operator new` allocates raw memory. It doesn't construct objects. Normally you just override `void* operator new(Allocator *alloc);` without any template stuff. If you need `T` inside your implementation of `operator new` you are doing something unusual and/or wrong. – n. m. could be an AI Dec 04 '16 at 05:21
  • An allocator would typically call `operator new`, not the other way around. Even if you did pass an allocator to `operator new`, `operator delete` wouldn't know how to free the memory when you're done with it. – Miles Budnek Dec 04 '16 at 05:35
  • Best I can tell from this mess, providing `MyClass::operator new` would solve your problem; maybe you could explain in more detail what didn't satisfy you about that. – M.M Dec 04 '16 at 05:39
  • Sounds like "placement new" (http://stackoverflow.com/questions/222557/what-uses-are-there-for-placement-new). – harper Dec 04 '16 at 08:16
  • Suppose you cam write a templated operator new. What would you do in its body? – n. m. could be an AI Dec 04 '16 at 18:34
  • @M.M You were right, I did not know the difference, however, now I do, so I have rewritten the question completely. – Nikita Dec 05 '16 at 01:59
  • "faster allocators"..."project group". May I guess you're a student who thinks he can beat professionals with decades of experience? – MSalters Dec 06 '16 at 13:04
  • @MSalters What value does your comment add to the question? And they ask me why I dislike the Dutch... – Nikita Dec 06 '16 at 23:43

4 Answers4

4

Kill new entirely. You have to bundle the creation with destruction anyhow.

template<class T>
struct destroy {
  Alloc* pool = nullptr;
  void operator()(T* t)const { 
    ASSERT(t);
    t->~T();
    ASSERT(alloc);
    alloc->Dealloc( t );
  }
};
template<class T>
using my_unique_ptr = std::unique_ptr<T, destroy<T>>;

namespace details{
  template<class T, class...Args>
  my_unique_ptr<T> my_make_unique( Alloc* alloc, Args&&...args ) {
    void* p_data = alloc->Allocate(sizeof(T), alignof(T));
    try {
      T* ret = ::new(p_data) T(std::forward<Args>(args)...);
      return {ret, destroy<T>{alloc}};
    } catch (...) {
      alloc->Dealloc( p_data );
      throw;
    }
  }
}
/// usual one:
template<class T, class...Args>
my_unique_ptr<T> my_make_unique( Alloc* alloc, Args&&...args ) {
  return details::my_make_unique<T>( alloc, std::forward<Args>(args)... );
}
// permit leading il:
template<class T, class U, class...Args>
my_unique_ptr<T> my_make_unique( Alloc* alloc, std::initializer_list<U> il, Args&&...args ) {
  return details::my_make_unique<T>( alloc, il, std::forward<Args>(args)... );
}
// for {} based construction:
template<class T>struct tag_t{using type=T;};
template<class T>using no_deduction=typename tag_t<T>::type;
template<class T>
my_unique_ptr<T> my_make_unique( Alloc* alloc, no_deduction<T>&&t ) {
  return details::my_make_unique<T>( alloc, std::move(t) );
}

now my_make_unique takes an Alloc* and construction arguments, and it returns a smart pointer with destruction code bundled.

This unique pointer can be passed to a std::shared_ptr<T> implicitly (via move).

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Probably MyClass should be replaced with T? – Eugene Dec 06 '16 at 00:32
  • @t.c. that overload is intended for {} based construction – Yakk - Adam Nevraumont Dec 06 '16 at 10:49
  • @Yakk Ah, I see. Still odd that `my_make_unique` sometimes work without an explicit template parameter. `template` and `U&& t`? – T.C. Dec 06 '16 at 10:58
  • @t.c. I'll just explicitly block deduction. – Yakk - Adam Nevraumont Dec 06 '16 at 11:55
  • @Yakk Excellent answer, though I'm used to seeing your posts on SO. I very much like the way you bundled the deleter with the allocated pointer, so we don't have to keep track of what is allocated by which specific allocator. Though I wanted to ask if there is a better way of dereferencing a smart pointer, perhaps overload operater-> somehow? Because when a class holds a `my_unique_ptr`, you can't access it like `(*my_obj) = (*foo).(*bar);` either. Any suggestions? – Nikita Dec 07 '16 at 10:47
  • @byrk I do not understand the follow up question. `->` works with smart pointers. `(*foo).(*bar)` doesn't work with anything. Better way than what? Better in what way? Comparing smart pointers to pointers? What are the types you of those tokens? – Yakk - Adam Nevraumont Dec 07 '16 at 13:00
  • @Yakk I meant when getting the value of a field `size_t size = (*(*(*object_ptr).m_ptr).m_ptr).m_size;` (worst-case scenario), would this be the only solution, or could you do something similar to regular, bare, pointers? example: `size_t size = object_p->m_p->m_p->m_size`. The latter is much cleaner, which I prefer. Or am I missing a key practice surrounding smart pointers and overthinking this?? – Nikita Dec 07 '16 at 17:03
  • @Byrk yes, `->` works with smart pointers? [`std::unique_ptr::operator->`](http://en.cppreference.com/w/cpp/memory/unique_ptr/operator*) – Yakk - Adam Nevraumont Dec 07 '16 at 17:09
  • @Yakk It seems resharper simply didn't suggest it, but it indeed works. And thank you for your answer, I appreciate the effort! – Nikita Dec 07 '16 at 20:18
0

If you are happy with this form of initialization:

MyClass *obj = new(alloc) MyClass(3.14);

That is, if you're fine with having to pass an object like that to new, then you can globally overload new with placement parameters:

void* operator new(std::size_t size, Allocator *alloc)
{
    return alloc->Allocate(size, alignof(std::max_align_t));
}

Any non-array use of new(alloc) will provoke a call to this function. Your big problem is the fact that there is no matching "placement delete" syntax. So to properly delete the object, you will need to do what you normally would for any other use of an object created through placement new:

obj->~Typename();
::operator delete(obj, sizeof(obj), alloc);

Of course, this requires an operator delete overload:

void operator delete(void *obj, std::size_t sz, Allocator *alloc)
{
    alloc->Deallocate(obj, sz, alignof(std::max_align_t));
}

If you have access to C++17, then operator new and operator delete can take the object's actual alignment as a parameter.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Currently working on [tag:visual-studio-2017]RC and I have not encountered the object's actual alignment *(I won't be using `max_align_t` any time soon)*, so I fear this won't help me, as I don't want poorly aligned objects in the first place, though this indeed seems ideal if working with [tag:c++17], which is not fully the case for me yet. – Nikita Dec 05 '16 at 11:40
  • @Byrk: "*I don't want poorly aligned objects in the first place*" Until C++17, that's how *all* memory allocation works. The default `operator new` and malloc always return memory aligned to the maximum possible alignment. – Nicol Bolas Dec 05 '16 at 15:59
0

You could make a factory template function. Sketch:

template<class R,class... T>
R* create(alloc& alloc,T&&... t)
{
    void *obj_mem = alloc.Allocate(sizeof(R), alignof(R));
    R* obj = new(obj_mem) R(std::forward<T>(t)...);
    return obj:
}
user3721426
  • 273
  • 2
  • 8
0

If you can (and want to) modify your allocators, I'd add a member function to to allocate and construct. In class SomeAllocator:

template <typename T, typename... Args>
T* SomeAllocator::Construct(Args&&... args)
{
    auto ptr = Allocate(sizeof(T), alignof(T));
    return new (ptr) T(std::forward<Args>(args)...);
}

This would allow you to use it like

auto obj = alloc->Construct<MyClass>(3.14);

For destruction and deletion you could - to provide a unified syntax - add a Destruct method:

template <typename T>
void SomeAllocator::Destruct(T*& t)
{
    t->~T();
    Deallocate(t, sizeof(T), alignof(T);
    t = nullptr;
}
Uroc327
  • 1,379
  • 2
  • 10
  • 28