There is something wrong with this code. Take some time to think about it.
The rule of thumb
As a C++ programmer, you might remember something like this from the past, when just starting your journey:
You: Sensei, I’m confused. What are those around the
string ? Why is it
int , but
const string& ?
Sensei: bla bla bla, some drawings on the board, bla bla bla
You: Oh, I understand (sure you do), Sensei. So how do I know when to use which?
Sensei: That’s simple. All those built in types, like int/bool/char, should be by value, and all the others, the classes, should be by const-ref. And yes,
string is not a built in type, it’s a class. There, even your Visual Studio shows them in different colors.
Or, maybe you didn’t have a Sensei, maybe it was just a Senpai. Maybe you are a self-taught programmer and StackOverflow was your Sensei. Anyway, you’ve come to a very simple rule, which is great to have, as a beginner. Simplicity helps in quick decision making, but it also gets stuck.
But it wasn’t over, there was still hope. Later you’ve (hopefully) read the “Effective C++: 55 Specific Ways to Improve Your Programs and Designs” by Scott Meyers. The “Item 20: Prefer pass-by-reference-to-const to pass-by-value.” is exactly about this. So you read it by nodding along, and here is the two-point summary of that item:
I bet you stopped paying attention after the “ … and STL ” and jumped to the next item.
I believe you’ve already figured out that the question we’re answering here is whether it’s correct to pass a
std::shared_ptr by const-ref or not?
A Prototype for the machine, an Interface for you
A function’s prototype is defined to be the return type and the number and types of the input parameters. It’s basically what you see in the declaration.
This is a simplified version of a well-know
std::getline function. This declaration contains the necessary information for the compiler. It needs to know the number of input parameters, their sizes and order to be able to generate the machine code for its calls. But it’s not just that, it also contains valuable information for humans, users of this function. Intuition suggests that
str is a so-called out parameter , meaning we expect the result, the ‘line’ to be stored in that
str . It would’ve been a different story if it was
const std::string& str) . But that extra ‘const’ doesn’t make any difference in the machine code generated. In fact, your compiler doesn’t care about const-s. Well, it kind of does when doing a name lookup, but that’s just a check, it doesn’t affect the generated machine code anyway. Const correctness is a beautiful concept and quite unique to C++ (some other languages tried, but nothing). It serves as a contract between those who define the interface and those who use it.
To sum up, you define your interfaces such that the users will understand you, what your intentions are.
Back to our code
When one says, the code is wrong , it doesn’t necessarily mean there’s a bug. It might perform poorly or have a so-called code smell . This term is a vast generalization of constructs that are technically valid, execute the intention correctly (thus are not bugs), but still alarm about a deeper issue. And by the way, these smelly constructs vary from language to language, from paradigm to paradigm.
To understand what’s wrong in our code, let’s quickly recap what
std::shared_ptr is. It’s a smart pointer that wraps the raw pointer along with a reference counter , providing a pointer-like interface. At any time the reference counter indicates how many
shared_ptr objects are there co-owning this resource. It increments every time a co-owner is copied and decrements every time a co-owner is destroyed (out of scope, destructed). Eventually, when this counter comes to 0, the raw pointer is deleted, the resource is freed.
With this in mind, I’ll go ahead and write that function’s declaration again, thinking out loud:
void foo(... widget);
I know the type will have
Widget somewhere somehow, but how exactly? Well, it depends. I ask myself the following questions:
There are 8 (2³) possible answer combinations to these questions, but because they are codependent, some of those combinations are irrelevant.
Before moving on, I suggest an experiment. Go over this table from the other way around: look at the argument and try to answer the questions. Imagine, you’re looking at a function with such an argument and try to figure out what was the intention behind.
So, not only the specific case dictates an interface, but also the latter explains a specific case. With this in mind, let’s finally get to our initial code.
Why don’t we have the case of the
const std::shared_ptr<Widget>& in our table? Is it because we missed a scenario? Let’s find out by working our way form the other side, let’s try to understand what someone wants to tell us with such an interface.
I understand this so:
- I want a pointer to a Widget , but not a regular one, I want a smart one, a shared one more specifically.
- As it’s not a
shared_ptr<const Widget>, I might modify the original object, so beware.
- Remember, when I said I want a shared pointer? Well, I’m not going to create a copy of it and become a co-owner. Basically, I’m not going to use the shared_ptr -ness of it, but if that object doesn’t happen to be wrapped in a shared_ptr , you’re supposed to make it happen. If so, you’ll most probably cause a double deletion .
Not only this doesn’t make sense but it’s also a legit code smell. It entails uncertainty and the potential of new bugs.
Being experienced enough in C++ you’ve probably seen const-refs a lot, so much that you’d hardly take some time to question their correctness, and mostly you’re right to do so. What makes shared_ptr so special is its unique philosophy. But it’s not the only thing in the language and STL that has a philosophy. Take a new perspective on them. For example, question why the
std::remove algorithm behaves the way it does and needs to be paired with erase ( erase-remove idiom ).
Pay more attention to the second point of the Item 20 and question it. It says “The rule doesn’t apply to … STL iterator and function types…”. Maybe there are some other cases too? After all, life has changed a lot since that book.
Martin Fowler once famously said: “ Any fool can write code that a computer can understand. Good programmers write code that humans can understand.”. There is way more to the code than just instructions to be understood by the machine. Well designed constructs have a purpose, philosophy, and a clean, expressive interface. Those are important to understand for appropriate usage.
Comments about the
weak_ptr in 3, 2, 1…