According to cppreference, std::vector::assign
replaces the contents with count
copies of value value
. This definition implies that the existing state of the vector is not used in assign
.
In the following example code run on GCC 14.2, test1
prints 20000002
. This indicates existing memory is deleted after the new space is allocated. With well-placed clear()
& shrink_to_fit()
calls, test2
reduces the peak object count and prints 10000002
.
Is this a missed optimization opportunity in the implementation of libstdc++
or is my understanding of std::vector
functions incorrect/incomplete?
#include <iostream>
#include <vector>
int curr;
int max;
void update(int delta) { curr += delta; max = std::max(max, curr); }
void reset() { curr = max = 0; }
struct Foo {
Foo() { update(1); }
Foo(const Foo&) { update(1); }
Foo& operator=(const Foo&) { return *this; }
~Foo() { update(-1); }
};
void test1()
{
reset();
std::vector<Foo> foos;
foos.assign(10'000'000, Foo{});
foos.assign(10'000'001, Foo{});
std::cout << max << '\n';
}
void test2()
{
reset();
std::vector<Foo> foos;
foos.assign(10'000'000, Foo{});
foos.clear();
foos.shrink_to_fit();
foos.assign(10'000'001, Foo{});
std::cout << max << '\n';
}
int main()
{
test1();
test2();
return 0;
}
The observed 20mil mean that there was a moment, when second assign
created objects, but objects of first assign
were not yet deleted.
And indeed, the second assign
is bigger. So if vector allocates exact amount of memory for first assign
, then for the second assign
it should create another block of memory. Where it will create second load of objects, increasing value of max
to 20 mil.
And then it releases memory allocated for first 10 mil elements.
It will not free the first memory block earlier, because during construction of the second memory block there may be an exception. So the first memory block is released just when it is guaranteed, that allocation and filling of the second memory block succeeded.
Your question is correct: assign
doesn't give a strong exception guarantee, i.e. if the second assign
would not require a reallocation, then there most probably wouldn't be a reallocation.
But if reallocation is inevitable, they go with a strong guarantee.
Here is explanation, what happened with numbers:
void test1()
{
reset();
std::vector<Foo> foos;
foos.assign(10'000'000, Foo{});
// allocated underlying memory for first bunch of elements
// constructed first 10mil elements
foos.assign(10'000'001, Foo{});
// allocated underlying memory for second bunch of elements
// constructed second 10mil elements,
// max reached 20mil
//then released first 10mil elements and their underlying memory
std::cout << max << '\n';
}
void test2()
{
reset();
std::vector<Foo> foos;
foos.assign(10'000'000, Foo{}); // constructed first 10 mil elements, `curr` == 10mil
foos.clear(); // deleted elements, reducing `curr` to 0.
foos.shrink_to_fit(); // released underlying memory
foos.assign(10'000'001, Foo{});
// allocated underlying memory for 10mil+1 elements
// constructed those elements, `curr` modified from 0 to 10mil+1
std::cout << max << '\n'; // prints 10mil+1
}