Reputation: 479
I have a simple Box container with a naive implementation that takes Car
#include <iostream>
#include <vector>
struct Car {
Car() { puts("def"); }
Car(Car const& other) { puts("copy"); }
Car& operator=(Car const& other) {
puts("assign");
return *this;
}
};
struct Box {
size_t size;
Car* ptr;
Box(size_t size)
: size(size)
, ptr{new Car[size]}
{}
Box(Box const& other)
: size{other.size}
{
ptr = new Car[size];
for (int i = 0; i < size; i++)
ptr[i] = other.ptr[i]; // hits operator=
}
};
int main() {
Box b(2);
Box b3 = b;
std::cout << std::endl;
std::vector<Car> v(2);
std::vector<Car> v2 = v;
}
o/p
def
def
def
def
assign
assign
def
def
copy
copy
Upvotes: 3
Views: 143
Reputation: 15
Perhaps used the reference counting mechanism in the implementation of the lower layers of the allocation.
Upvotes: -2
Reputation: 29023
new[]
combines allocating memory with starting the lifetime of the elements in the array. This can be problematic, as you've seen, because it calls the default constructor of each element.
What std::vector
does is use std::allocator
(or whatever allocator you provided as the second template argument) to allocate memory then uses placement new
to start the lifetime of the array's elements one-by-one. Placement new is a new expression where the developer provides a pointer to where the object should be created, instead of asking new
to allocate new storage.
Using this approach, here is a simplified example of your copy constructor :
Box::Box(const Box & other) : size{other.size}
{
// Create storage for an array of `size` instances of `Car`
ptr = std::allocator<Car>{}.allocate(size);
for(std::size_t i = 0; i < size; ++i)
{
// Create a `Car` at the address `ptr + i`
// using the constructor argument `other.ptr[i]`
new (ptr + i) Car (other.ptr[i]);
}
}
With this approach, you can't use delete[]
or delete
to clean up your Car
elements. You need to explicitly perform the previous process in reverse. First, explicitly destroy all the Car
objects by calling each of their destructors, then deallocate the storage using the allocator. A simplified destructor would look like :
Box::~Box()
{
for(std::size_t i = 0; i < size; ++i)
{
// Explicitly call the destructor of the `Car`
ptr[i].~Car();
}
// Free the storage that is now unused
std::allocator<Car>().deallocate(ptr, size);
}
The copy assignment operator will involve both of these processes, first to release the clean up the previous elements, then to copy the new elements.
Here is a very rudimentary implementation for Box
: https://godbolt.org/z/9P3sshEKa
It is still missing move semantics, and any kind of exception guarantee. Consider what happens if new (ptr + i) Car (other.ptr[i]);
throws an exception. You're on the line for cleaning up all the previously created instances, as well as the storage. For example, if it throws at i == 5
you need to call the destructors of the Car
objects 0 through 4, then deallocate
the storage.
Overall, std::vector
does a lot of heavy lifting for you. It is hard to replicate its functionalities correctly.
Upvotes: 5
Reputation: 29965
std::vector
uses an allocator instead of using the new
operator. The crucial difference is that the new
operator constructs every single element in the array. But vector allocates raw memory and only constructs elements on demand. You could achieve the same by using operator new
(instead of new
operator), malloc
, some allocator, or by other means. You then use placement-new to call the constructors. In destructor, you have to call destructors of all elements individually and only then free the memory. See:
Additionally, your Box
class needs an operator=
since the default one does something wrong. And a destructor. It's leaking memory.
Upvotes: 3