Recently, I had a bug report against libc++’s implementation of deque
.
The code in question was (simplified):
#ifndef _LIBCPP_NO_EXCEPTIONS
try
{
#endif // _LIBCPP_NO_EXCEPTIONS
__buf.push_back(__alloc_traits::allocate(__a, __base::__block_size));
#ifndef _LIBCPP_NO_EXCEPTIONS
}
catch (...)
{
__alloc_traits::deallocate(__a, __buf.front(), __base::__block_size);
throw;
}
#endif // _LIBCPP_NO_EXCEPTIONS
and the bug report read “If the allocation fails, then the pointer never gets
added to __buf
, and so the call to deallocate
will free the wrong thing.”
Thanks to Tavian Barnes for the bug report and the diagnosis.
There are two operations here that can fail; the first is the allocation,
and the second is the call to push_back
. We need to deal with both.
Tavian suggested declaring a variable named __block
(since this is allocating
block of memory for the deque
to store objects into), and then attempting to
add that to __buf
. If that fails, then deallocate __block
.
I dismissed this solution immediately, because __block
has a special meaning
in Objective-C, and there are people who use libc++ and Objective-C/C++ together.
However, I did try writing with __ablock
instead:
#ifndef _LIBCPP_NO_EXCEPTIONS
pointer __ablock;
try
{
#endif // _LIBCPP_NO_EXCEPTIONS
__ablock = __alloc_traits::allocate(__a, __base::__block_size);
__buf.push_back(__ablock);
#ifndef _LIBCPP_NO_EXCEPTIONS
}
catch (...)
{
__alloc_traits::deallocate(__ablock);
throw;
}
#endif // _LIBCPP_NO_EXCEPTIONS
Testing this, I found that this solved the problem. Yay!
But the code was still ugly. All those ifdefs and the catch (...)
bothered me.
Then I remembered unique_ptr
, and realized that it did exactly what I wanted.
Libc++ has an internal class called __allocator_destructor
, whose constructor
takes an allocator (by reference) and a size_t, and whose operator()
takes a
pointer and deletes it, using the allocator’s deallocate
function (passing in
the size). This is used in several places in libc++.
Suddenly, the code got much simpler:
typedef __allocator_destructor<_Allocator> _Dp;
unique_ptr<pointer, _Dp> __hold(
__alloc_traits::allocate(__a, __base::__block_size),
_Dp(__a, __base::__block_size));
__buf.push_back(__hold.get());
__hold.release();
This looks very different – what’s going on here?
This code allocates the block of memory and immediately stuffs it into a
unique_ptr
(__hold
). Then it adds it to __buf
as before. Then we release
it from __hold
, which says that it doesn’t need to be deallocated when __hold
goes out of scope.
What about when an exception is thrown?
* If the allocation throws, the __hold
is never constructed. Nothing was
allocated, and nothing needs to be deleted.
* If the push_back
throws, then __hold
‘s destructor deallocates the memory for us.
In either case, the right thing happens.
The general pattern for this is:
unique_ptr<T> holder(new T{/* params */});
// set up some stuff that uses holder.get()
holder.release();
If the “stuff” throws, then holder cleans up. Simple, and no need for try/catch
blocks to handle deallocations.
If you’re writing code that does allocations and other operations that can throw,
this pattern could simplify your code a lot.