Does v.clear()
free the allocation?
v.clear()
free the allocation?A perennial question on the cpplang Slack:
“When I call v.clear()
on a vector, does that hang onto the heap allocation,
or free it?” The short answer is: “It hangs onto the allocation. Follow up with
v.shrink_to_fit()
if you need the allocation eagerly returned.”
Now here’s the long answer!
In general, the STL tries not to do things you didn’t ask for. When you’re
actually done with the vector, of course its destructor will both destroy
the vector’s elements and free the heap allocation. But when you
merely call v.clear()
, you’re saying you’re not done with the vector —
you might put some elements into it again. Therefore it would be silly
for the STL to free the old heap allocation, because it’ll just need to make
a new heap allocation again for those new elements. By hanging onto the
vector’s old heap allocation, we save one round-trip to the heap.
When your intent is to actually free the block — that is, to
reduce memory usage as much as possible — you should use
the dedicated intent-expressing method v.shrink_to_fit()
. That is, first you
clear
the vector to reduce its size
to zero, and then you shrink_to_fit
it
to reduce its capacity
also to zero. You can also shrink_to_fit
a non-empty
vector:
v = {1,2,3};
v.shrink_to_fit();
This makes v.capacity()
equal to 3 (with the usual caveat that technically
the behavior is implementation-defined; but all vendors get this part right).
Notice that this requires the library to make a new memory allocation of size 3
before it can give back the original allocation! That’s right: shrink_to_fit
doesn’t just mean “please free unused blocks,” it means “please relocate my data
into new, smaller, blocks so that you can free the older larger blocks.”
Thus shrink_to_fit
invalidates pointers and iterators.
P0447 “
std::hive
” introduces a new verb,v.trim_capacity()
, to mean “please free unused blocks without relocating elements or invalidating iterators.” This new verb is almost meaningless tovector
; but it is very meaningful todeque
, and if P0447 is accepted,deque
should certainly also gain atrim_capacity
method.
How do I clear and free in one line?
Usually whoever’s asking about v.clear()
won’t take shrink_to_fit
for an answer.
Everyone’s looking for a one-liner here. So they ask: How about v.resize(0)
? How about v = {}
?
How about…?
Here (Godbolt, backup) is a test program running through each STL vendor’s implementation of each relevant container. Unfortunately this produces a table with three independent dimensions, which I have to squeeze into two for presentation.
Code | vector |
string |
deque |
---|---|---|---|
v.clear() |
❌ | ❌ | MS |
v.resize(0) |
❌ | ❌ | ❌ |
v = {} |
❌ | ❌ | ❌ |
v = V() |
✅ | libc++, MS | ✅ |
V().swap(v) |
✅ | ✅ | ✅ |
The sole one-liner that reliably works for all three containers, on all three vendors’ implementations,
is V().swap(v)
— the same one-liner that’s been known since C++98. It’s covered in Item 7.3 of Herb Sutter’s
More Exceptional C++ (2001), for example.
Only Microsoft’s deque
currently treats d.clear()
as a request to free the deque’s allocation.
(So only Microsoft ever treats clear
any differently from resize(0)
.)
But notice that Microsoft is also the only vendor where deque
can hang onto an arbitrarily large
number of unused blocks! libc++’s and libstdc++’s deque
will never keep more than two unused blocks.
In fact, libstdc++’s deque
is “never-empty”: it always hangs onto one block’s worth of storage,
even in its “emptiest possible” default-constructed state.
That’s why libstdc++ trunk manually warrants its
deque
as trivially relocatable: its move-constructor may throw. It would get the vector pessimization, if not for that manual marking. Still, wrapping libstdc++’sdeque
in a Rule-of-Zero type reinstates the vector pessimization (unless you have P1144): Godbolt.Microsoft’s
list
has the same issue as libstdc++’sdeque
, but stoically accepts its vector pessimization: Godbolt.
deque
’s heap-management strategy is complicated, highly varied from vendor to vendor,
and definitely a story for another day.
Finally, we might ask: What is the type of the right-hand operand in v = {}
?
This trivia question fooled me! You might already know the surprising factoid that
an initialization like A a = {}
never calls an initializer-list constructor — it
calls A
’s default constructor instead. So you might think a = {}
would work the
same way. (I did!) But we’d be wrong: In fact the special case for {}
is
codified in [dcl.init.list] and thus applies
only to initialization. The assignment-expression a = {}
, like any other function call such as f({})
,
prefers to bind {}
to a parameter of type initializer_list
, rather than to a parameter of type A
.
(Godbolt.) So a = {}
means something different from — and
generally less efficient than — a = A()
.