Vegeta
Vegeta

Reputation: 479

This might be a naive question but in simple terms how to implement a copy constructor same as std::vector?

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

Answers (3)

Alireza
Alireza

Reputation: 15

Perhaps used the reference counting mechanism in the implementation of the lower layers of the allocation.

Upvotes: -2

Fran&#231;ois Andrieux
Fran&#231;ois Andrieux

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

Aykhan Hagverdili
Aykhan Hagverdili

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

Related Questions