A trivially copyable ticket for a unique_ptr
unique_ptr
In Chandler Carruth’s CppCon 2019 talk “There Are No Zero-Cost Abstractions,”
he talked a
lot (not exclusively, but a lot) about the hidden performance cost of std::unique_ptr
.
See, unique_ptr
may be the same size as a native pointer, but because it has a
user-defined destructor, it is “non-trivial for purposes of ABI,” and that means
it has to get passed on the stack.
For more on “trivial for purposes of ABI,” see my previous blog post
”[[trivial_abi]]
101” (May 2018).
It occurred to me afterward that even if you didn’t want to write your own custom [[trivial_abi]]
-enabled
MyUniquePtr
(maybe because you valued GCC compatibility — I think GCC still hasn’t implemented
that attribute), you might be able to hack around it by simply creating a trivially copyable
“ticket for a unique_ptr
,” in the same sense that I describe weak_ptr
as “a ticket for a
shared_ptr
.” The important thing about a ticket is that you can’t use it directly; it doesn’t
afford the user any operation except “redeem this ticket for a unique_ptr
.”
It would look something like this (Godbolt):
template<class T>
struct ticket {
ticket() = default;
explicit ticket(T *p) : p_(p) {}
explicit ticket(std::unique_ptr<int>&& p) : p_(p.release()) {}
std::unique_ptr<T> redeem() && { return std::unique_ptr<T>(std::exchange(p_, nullptr)); }
private:
T *p_ = nullptr;
};
And then Chandler’s test harness, which originally looked something like this and produced 27 lines of assembly —
void bar(int*);
void baz(std::unique_ptr<int>);
void foo(std::unique_ptr<int> p) {
bar(p.get());
baz(std::move(p));
}
— would instead look something like this and produce 19 lines of assembly —
void bar(int*);
void baz(ticket<int>);
void foo(ticket<int> t) {
std::unique_ptr<int> p = std::move(t).redeem();
bar(p.get());
baz(ticket<int>(std::move(p)));
}
So what’s the catch? Well, it’s a big one. The trivially copyable ticket
object
by definition has a trivial destructor. So it doesn’t consider itself to “own” the
heap-allocated object. If an exception is thrown during the time the heap allocation
is managed only by the ticket, then the allocation will be leaked!
void use(ticket<int> t, int u);
int thrower() { throw "oops"; }
void test() {
auto p = std::make_unique<int>(42);
use(
ticket<int>(std::move(p)), // lose ownership...
thrower() // ...and leak the allocation!
);
}
However, as Chandler himself pointed out, this is only a problem if your codebase
uses exceptions at all! If you don’t use exceptions, then you don’t have this issue,
and maybe the idea of a “trivially copyable ticket for a unique_ptr
” might be
interesting to you.
What would make this pattern actually usable, I think, would be if the language had some
way to say “ABI-wise, I take a parameter of type X
; but the first and only thing
I’m ever going to do with that parameter is to convert it to type Y
.” Something like
this fantasy syntax:
void baz(ticket_view<int>);
void foo(ticket_view<int> -> std::unique_ptr<int> p) {
bar(p.get());
baz(std::move(p));
}
(Here I’ve renamed ticket
to ticket_view
, and given it an implicit constructor
from unique_ptr
, and given it an explicit conversion to unique_ptr
instead of
a named method redeem()
. This emphasizes its similarity to string_view
as a
parameter-only type.)