3

C++ Allocator. I know that String will allocate a block buffer with new at inner implement, and free it(call delete[]) within Destructor.

My question is that whether it will be free double when use allocator<string>?

  1. first free at string destructor.
  2. second deallocate buffer points to string has been free.

Additionally, whether does the buffer address of string and allocate(n) have same region?

#include <iostream>
#include <memory>
#include <string>

using namespace std;

int main(int argc, char **argv)
{
    const int cnt = 10;
    allocator<string> alloc;
    auto p = alloc.allocate(cnt);

    alloc.construct(p);
    for (int i = 0; i < cnt; ++i)
    {
        cout << p+i << endl; // print buffer address
    }

    alloc.destroy(p); // will it free buffer of string?

    alloc.deallocate(p, cnt); // will it free buffer of string again?

    return 0;
}
Charles
  • 59
  • 5

3 Answers3

6

Let's start with delete

When you delete an object there are two things that happen:

  1. the destructor of that's object is called
  2. the memory allocated for this object with new is being released back to the heap

placement new and delete

You can use placement new syntax for constructing an object on an existing memory buffer:

const char* charString = "Hello, World";
// allocate the required memory
void *mem = ::operator new(sizeof(Buffer) + strlen(charString) + 1);
// construct a "Buffer" object on an existing memory block
Buffer* buf = new(mem) Buffer(strlen(charString));

// ...

// destruct the "Buffer" object without releasing the memory
buf->~Buffer();
// deallocate the memory
::operator delete(mem);

Of course for this example you could just use plain new and delete but it shows how you can separate the memory allocation from its construction and the destruction from the deallocation.

This technique is useful for example if you manage memory pool and the memory can be reclaimed back to the memory pool when the object is destructed rather than back to the heap.

std::allocator

std::allocator gives you the above behavior - separation of memory allocation and deallocation from the object construction and destruction with the methods allocate, construct, destroy and deallocate.

allocator<string> alloc;
size_t n = 3;
auto p = alloc.allocate(n); // memory is allocated for n elements
alloc.construct(p);           // object is constructed on that memory

// ...

alloc.destroy(p);         // object is destructed
alloc.deallocate(p, n); // memory is deallocated

Note that std::string destructor would call delete on its internal allocation when the string object is destructed, but the memory occupied by the object itself is deallocated in above code only in the call to alloc.deallocate.


construct and destroy being deprecated in C++17

C++17 declared the methods construct and destroy of std::allocator deprecated and C++20 made them obsolete. So, since C++17 you would have to either go back to placement new, or better (thanks @Evg for the comment) - use std::allocator_traits::construct() which has the benefit of being constexpr since C++20. For the destruction, call the destructor directly after deallocating the memory with the allocator:

allocator<string> alloc;
size_t n = 3;
auto p = alloc.allocate(n); // memory is allocated for n elements
// alloc.construct(p);        // deprecated in C++17, obsolete in C++20
// option a:
   // new(p) string; // construct the object with placement new
// option b, better - (potentially) constexpr since C++20:
std::allocator_traits<allocator<string>>::construct(alloc, p, "hello");

// ...

// alloc.destroy(p);      // deprecated in C++17, obsolete in C++20
p->~string(); // destruct the object by calling the destructor
alloc.deallocate(p, n); // memory is deallocated

Code link


construct_at and destroy_at *

* since C++20 and C++17, respectively.

With construct_at and destroy_at the code above can be written as:

allocator<string> alloc;
size_t n = 3;
auto p = alloc.allocate(n); // memory is allocated for n elements
std::construct_at(p, "hello"); // added in C++20

// ...

std::destroy_at(p); // destruct the object - added in C++17
alloc.deallocate(p, n); // memory is deallocated

This one is the simplest, if you are in C++20 I'd recommend this option.

Code link


Last note: there is also a thing called placement delete it is not called directly by the programmer but only from placement new if the constructor of the object throws an exception. So don't get confused with that. The way to destruct an object in place without releasing the memory occupied by the object itself is by calling its destructor directly as shown above (or if you use std::allocator prior to C++17, by calling the allocator destroy method).

Last and final note: if you wonder why deallocate require a size? see: why does std::allocator::deallocate require a size?

Amir Kirsh
  • 12,564
  • 41
  • 74
  • +1 This is a very detailed answer. But i had to search a bit and understand two things before i could grasp the answer: 1) "operator new()" is different than "new" ("operator new" is a function that just allocates memory) AND 2) "placement new" is different than "new". ("placement new" just constructs an object in a given place) – CharMstr Jan 21 '21 at 23:09
  • Since C++17, `std::allocator_traits` should be used, not placement `new`. In C++20, placement `new` can't be used in `constexpr` context, whereas [`std::allocator_traits::construct()`](https://en.cppreference.com/w/cpp/memory/allocator_traits/construct) can. – Evg Nov 18 '22 at 15:15
  • @Evg good point, embedded into the answer with a proper credit note. – Amir Kirsh Nov 18 '22 at 16:45
5

When you write something like this:

Foo *foo = new Foo();

two things happen:

  1. Some heap space is allocated for the Foo object.
  2. The Foo() constructor is invoked, with this pointing to the newly-allocated space.

Later, you delete the Foo object:

delete foo;

and two more things happen:

  1. The destructor ~Foo() is invoked.
  2. The memory allocated for the Foo instance is released back to the heap.

The std::allocator class just lets you perform each of these four steps manually.

If you have an allocator<string> alloc and you call alloc.allocate and then alloc.construct, that's just the same as doing new string(). And when you call alloc.destroy and then alloc.deallocate, that's just the same as deleting the string pointer.

So no, there won't be any extra frees going on. The call to destroy causes the string to release whatever memory it allocated for its buffer, and then the call to deallocate releases the memory that was used for the string object itself.

I didn't completely understand your question about the region. The memory allocated to store the string instance, and the memory that the string allocates for its buffer, are unrelated.

Willis Blackburn
  • 8,068
  • 19
  • 36
1

You may inherit std::string to add some print statements, to get a better understanding (Or you need to debug and step into the STL container and functions)

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class MyString : public string {
    public:
    MyString() = default;
    ~MyString() { cout << "destructor of MyString:" << this << endl; }
};
int main(int argc, char **argv)
{
    const int cnt = 10;
    allocator<MyString> alloc;
    auto p = alloc.allocate(cnt);

    alloc.construct(p);
    for (int i = 0; i < cnt; ++i)
    {
        cout << p+i << endl; // print buffer address
    }

    alloc.destroy(p); // will it free buffer of string?

    cout << "alloctor destrory called" << endl;

    alloc.deallocate(p, cnt); // will it free buffer of string again?

    return 0;
}

Execute it here:

https://coliru.stacked-crooked.com/a/13302c7f0c4db07b

prehistoricpenguin
  • 6,130
  • 3
  • 25
  • 42