3

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
  • std::vector copies and calls the copy constructor and Box doesn't. How std::vector's copy constructor is implemented? and what I am doing wrong?
  • How std::vector's memory allocation is handled and why default constructor is called only two times in std::vector where as 4 times in Box? Any explanation would suffice
Vegeta
  • 239
  • 5
  • 1
    You're missing a user-defined assignment operator for `Box`: `Box& operator=(Box b) { std::swap(b.size, size); std::swap(b.ptr, ptr); return *this; }` – PaulMcKenzie May 20 '21 at 15:51
  • 1
    Also a destructor for `Box` – Mooing Duck May 20 '21 at 15:54
  • BTW, `#include ` is for C++ I/O, like `std::cin` and `std::cout`. You'll want to `#include ` for using the C I/O, like `puts` and `scanf`. – Thomas Matthews May 20 '21 at 16:35
  • Should we tell Vegeta the bad news, that there is no way to implement what std vector does under the [tag:C++11] standard due to defects/oversights in said standard, but that doing certain things despite violating the standard actually works in every compiler? Or should we just pretend that this isn't the case and let them live a life of tranquility? Tranquility it is. Consider it to be like being taught newton's laws of motion, even though they aren't true at high energy scales. – Yakk - Adam Nevraumont May 21 '21 at 15:24

3 Answers3

5

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.

Chris Uzdavinis
  • 4,167
  • 4
  • 14
François Andrieux
  • 24,129
  • 6
  • 46
  • 72
  • Thanks for the answer. As you mentioned, I don't get why can't we use delete[] or delete and this expression new `(ptr + i) Car ()` I've never seen it. Can you enlighten explain about this? – Vegeta May 20 '21 at 17:22
  • @Vegeta From the answer : "[Placement new](https://en.cppreference.com/w/cpp/language/new#Placement_new) is a new expression where the developper provides a pointer to where the object should be creater, instead of asking new to allocate new storage." Normally `new` allocates storage, then constructs an object there. The argument before `Car` is a pointer where, instead of allocating anything, `new` should just construct the object directly at that location. – François Andrieux May 20 '21 at 17:39
  • @Vegeta `delete` is like `new`, it tries to manage memory internally. But when you use placement `new` the memory was not allocated by `new` so `delete` wouldn't know what to do. It would be similar to trying to do `Car c; delete &c;`. It just doesn't work and you get Undefined Behavior. – François Andrieux May 20 '21 at 17:40
3

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.

Ayxan Haqverdili
  • 17,764
  • 5
  • 27
  • 57
-2

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

Alireza
  • 15
  • 5