•
26 June 2025
•
4 mins
Recently, I encountered a subtle performance issue while refactoring some C++ code. I was passing a std::shared_ptr
void caller(std::shared_ptr<T> shared_ptr) {
...
callee(shared_ptr);
}
void callee(std::shared_ptr<T> ptr) {
auto foo = ptr->read_value() + 1;
}
Infer’s documentation around PULSE_READONLY_SHARED_PTR_PARAM
says the following:
This issue is reported when a shared pointer parameter is a) passed by value and b) is used only for reading, rather than lifetime extension. At the callsite, this might cause a potentially expensive unnecessary copy of the shared pointer, especially when many number of threads are sharing it. To avoid this, consider 1) passing the raw pointer instead and 2) use
std::shared_ptr::get
at callsites.
So, its suggestion was to change my code to the following:
void caller(std::shared_ptr<T> shared_ptr) {
...
callee(shared_ptr.get());
}
void callee(T* ptr) {
auto foo = ptr->read_value() + 1;
}
Sure, Infer got the diagnosis right, but the proposed solution of using raw pointers seems wrong. Smart pointers (unique_ptr and shared_ptr) were introduced precisely to avoid the many footguns associated with raw pointer memory management and safety. There should be almost no good reason to use raw pointers, and the use case above seems too trivial to warrant using a raw pointer. In fact, if I passed a raw pointer, some future developer might wrap it in a new shared_ptr and pass it elsewhere. That’s a recipe for double-free bugs and a nasty core dump.
Ranting aside, I still couldn’t let the change stand as-is; the infer error was pointing to a legitimate problem. So, how do we address this issue without resorting to raw pointers? Answer: const refs :)
Instead of passing the raw pointer, what if we just passed the underlying object itself? Well, we don’t really need a copy of the object, and so we can pass a reference to it. Since we’re only calling a read-only method, a const reference works just fine. With that, we have this fix:
void caller(std::shared_ptr<T> shared_ptr) {
...
callee(*shared_ptr);
}
void callee(const T &obj) {
auto foo = obj.read_value() + 1;
}
And it works like a charm.
Of course, passing a const reference to the object isn’t the only way to avoid copying the shared pointer. You can also simply pass the shared pointer by reference!
void caller(std::shared_ptr<T> shared_ptr) {
...
callee(shared_ptr);
}
- void callee(std::shared_ptr<T> ptr) {
+ void callee(const std::shared_ptr<T> &ptr) {
auto foo = ptr->read_value() + 1;
}
(I showed this as a diff because the change is subtle! Also note: taking a reference to a shared_ptr
avoids bumping the reference count, so there’s no added contention.)
Now, be careful here. You can’t use this trick if shared_ptr
could be a nullptr
, because passing a reference to a nullptr
and trying to dereference it is a great way to bring down your service! In my case, it turns out that shared_ptr
was guaranteed to be non-null, and so this trick works well.
So next time someone suggests using a raw pointer, be skeptical—there’s almost always a safer alternative to that particular footgun.
I’ll stop here, but don’t walk away thinking const refs are a cure-all. They can backfire too. This post in Belay the C++ outlines some of the less obvious pitfalls.
Like it? Share it!