What's behind the curtain of std::vector range initialization?

202 views Asked by At
#include <utility>
#include <vector>
#include "iostream"

class Person {
public:
    std::string name{"no-name"};

    Person() {
        std::cout << std::string("Person()") << std::endl;
    }

    explicit Person(std::string name) : name(std::move(name)) {
        std::cout << std::string("Person(") + this->name + ")" << std::endl;
    }

    // operator =
    Person &operator=(const Person &other) {
        std::cout << std::string("operator= ") + other.name + "->" + this->name + "" << std::endl;
        return *this;
    }

    Person(const Person &other) noexcept {
        std::cout << std::string("copy ") + other.name + "->" + this->name + "" << std::endl;
        this->name = other.name;
    }

    // move
    Person(Person &&other) noexcept {
        std::cout << std::string("move ") + other.name + "->" + this->name + "" << std::endl;
        this->name = std::move(other.name);
    }

    ~Person() {
        std::cout << std::string("~Person(") + name + ")" << std::endl;
    }

};

int main() {

    std::cout << "construct person_list" << std::endl;
    std::vector<Person> person_list (4);

    std::cout << "construct person_list_copy" << std::endl;
    std::vector<Person> person_list_copy (person_list.begin(), person_list.end());
}

output:

construct person_list
Person()
Person()
Person()
Person()
construct person_list_copy
copy no-name->no-name
copy no-name->no-name
copy no-name->no-name
copy no-name->no-name
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)

Where does the enough space of vector "person_list_copy" come from before the copy operation took place?

I think the vector is much like a C-style fixed-size array, we firstly need to have enough space where I think the default constructor should be called, and then we assign some objects into those pre-constructed "cells", at which I think copy constructor should be called.

The code above directly creates object with copy constructor, but how is that possible to put an object into an array before the space of the array is constructed?

I never saw something like this before in terms of C programming. And I can't figure out an available way to achieve something like what std::vector does with all terminologies I've got so far, like 'new', uint8_t fixed_array[LEN], malloc.

I'm a C++ beginner, and there must be some misunderstanding of vector range initialization in my head, or may be there are some magics I don't know yet about C++ and STL.

Expected output

construct person_list
Person()
Person()
Person()
Person()
construct person_list_copy
Person()
Person()
Person()
Person()
copy no-name->no-name
copy no-name->no-name
copy no-name->no-name
copy no-name->no-name
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
~Person(no-name)
2

There are 2 answers

6
MBobrik On

Well, that's what C++ can do and does if possible. Allocates empty memory first, calls constructors later. std classes are intentionally written to do this to save CPU time.

In C++ you have full control over the raw memory and you can choose to do with it as you please. Including stuff like leaving it uninitialized or forcibly calling initialization on an arbitrary chunk of memory.

See https://www.geeksforgeeks.org/placement-new-operator-cpp/ for example of how it can be done under the hood.

2
Weijun Zhou On

How std::vector constructs the new elements depend on the template parameter Allocator. std::vector<T, Allocator> calls Allocator::allocate to reserve the memory and Allocator::construct(Before C++11) or std::allocator_traits<Allocator>::construct(Since C++11) to actually construct the elements. These functions take an address to construct the object along with the parameters to be forwarded to the actual constructor, and should construct the element in place.

The memory allocation and the element construction are two separate steps and there is no need to default-construct the elements first. If the elements have to be default constructed first, it would not be possible to use std::vector for non-default-constructible types, but it is perfectly valid to have std::vector<S> where S is not a default-constructible type. In fact, the requirement for the element_type used for std::vector depends on the actual functions to be called, but the bare minimum is that the element is Erasable. Default constructible is a much more strict condition than erasable and is not required.

The default parameter for Allocator is std::allocator. For std::allocator, both Allocator::construct (before C++11) and std::allocator_traits<Allocator>::construct (after C++11, and via the call to std::construct_at after C++20) both ultimately calls placement new to construct the objects in already allocated memory. If you use a custom Allocator template parameter, it is possible that default construction is performed before construction of actual element, but it is unusual to do so and not efficient anyway.

Specifically addressing your misunderstandings:

Where does the enough space of vector "person_list_copy" come from before the copy operation took place ?

It's from Allocator::allocate() (before C++11) or std::allocator_traits<Allocator>::allocate() (after C++11). For std::allocator, this is equivalent to calling operator new(). I think you may be confused about the new keyword and the global allocation function operator new(). The latter only allocates uninitialized memory and is quite similar to std::malloc, except that it has to be paired with operator delete instead of std::free.

There are already many duplicates addressing the difference between new keyword and operator new, but to briefly recap, when you do p=new Person;, first operator new() is called to allocate the memory, then the default constructor is called. However, it is perfectly valid to just call operator new() to allocate the memory without invoking any constructor, and that is exactly what is happening for std::vector allocation with std::allocator.

I can't figure out an available way to achieve something like what std::vector does with all terminologies I've got so far, like 'new', uint8_t fixed_array[LEN], malloc.

I guess the missing piece is operator new() and how it is different from a new expression.