Pass-by-value vs. pass-by-reference-to-const in C++
medium.com
medium.com
Suppose you have a class
class Foo {
shared_ptr<Widget> widget;
bool has_widget();
void set_widget(shared_ptr<widget> widget);
}
and you are writing a function which may call set_widget(), it can save an unnecessary shared_ptr copy if you pass it as a const reference: void maybe_set_widget( Foo& foo, const shared_ptr<Widget>& widget) {
if(!foo.has_widget()) {
foo.set_widget(widget);
}
}
Doing a quick google search finds that Herb Sutter agrees with me (https://herbsutter.com/2013/06/05/gotw-91-solution-smart-poi...):>Use a const shared_ptr& as a parameter only if you’re not sure whether or not you’ll take a copy and share ownership
The main performance ‘problem’ with shared pointers is locking and contention in heavily multithreaded scenarios, which is why pretty much nobody uses a reference counting memory model for these kinds of things. In the vast majority of single or lightly threaded scenarios I think you will be hard pressed to find any reasonable algorithm implemented using shared pointers where the reference counting overhead is even measurable. So it’s not a strange thing at all to deprioritize hypothetical performance gains over clear intent of your code. Best thing is to stay away from shared pointers unless you really need them anyway, as they promote designs with sloppy ownership semantics. Been there, done that, try to get rid of them years later... :-/
The almost obligatory Herb Sutter reference made me chuckle by the way. He says so many things about what he thinks is the right thing to do in C++, and gets quoted almost as an absolute authority on good C++ practices, but quite often (not always, mind you) I find his arguments weak and unconvincing, and his recommendations sometimes downright counterproductive.
Could you elaborate? I'm not an expert in C++ but smart pointers did simplify a lot of my code and I never met the case where it was a problem.
Like everything else in C++ (and any low-level language), they should be used carefully.
It is also very easy to accidentally create cyclic references with shared pointers. Suppose you have a class like this:
struct Node {
shared_ptr<Node> parent;
shared_ptr<Node> child;
};
This rather innocent looking class creates a cycle when you fill in the values, because the parent and child reference each other. That cycle now means your nodes will not be cleaned up anymore.Unique pointers do not have any of these problems. They cannot create cycles, are very cheap and they make it very explicit who owns what. They are not suitable for every situation (i.e. when there is actually shared ownership shared pointers should be used) but in most situations I find that they are sufficient and have a lot less pitfalls than shared pointers.
I don't think this is true. Couldn't you accidentally make two objects "own" each other?
It’s fairly common knowledge, but just to answer the inevitable question: you can avoid the cycle with weak pointers.
If you don’t know what the lifetime of your objects is, it is very difficult to follow RAII best practices, to control memory use (a shared pointer that is held on to where it is not necessary is basically a leak), to avoid high and/or unpredictable allocation/deallocation cost (compared to e.g. arena allocators or just stack copies), etc. On top of that there is a very conceivable risk of introducing cycles (and hence leaks) when overusing shared pointers. And last but not least debugging code that defaults to shared pointers everywhere can be a nightmare.
These are all practical downsides, but to me personally the argument that you have to be explicit about your memory model (allocation, ownership, lifetime) and use the right tools for your design is even more important. Shared pointers are definitely a very useful tool, but only in situations where you truly want to have shared ownership, and not just because they are convenient because they save you from having to think about these things. Over time I’ve found that more often than not, using strictly unique_ptr for ownership, raw pointers and references to avoid copies, and plain value copies where appropriate and not performance critical, leads to code that is much easier to reason about, has less bugs, etc.
The pool dequeued a task, issued it to a thread, the thread then called a method on the object and asked for more work.
I stumbled over a case where the destructor of the task was not always called, leaving all sorts of handles hanging.
Turned out the code that dequeued a task did so into a local variable, and didn't clear that variable after it had passed it on to the thread. So when the pool ran out of work, a reference to the last task was being held by the local variable in the pool code, and that reference was only released when new work arrived or the pool was destroyed.
However, you missed a _super_ important caveat of this which is very subtle and very awful. And that is all of the code being executing here between the start of the function and the copy of the shared_ptr is doing it without the protection of the reference count on the shared pointer. It's entirely conceivable that, for example, the `has_widget` function, while executing, invalidates the original shared_ptr passed in and thus makes your eventual use of that shared_ptr invalid.
More realisticly, if you pass a reference to a different thread and return there is no way when to know when the original goes out of scope. Don't do that.
you don't need threads for what you're describing: simply passing it to a same-thread method/function which creates a heap-based copy will already complicate your understanding of the lifetime of the referenced object.
concrete example: we make wide use of the so-called "signals&slots" pattern, which is more accurately denoted as "anonymous callbacks". we have had a consistent source of serious lifetime mgmt problems caused by (boost|sigc|std)::bind()-ing a shared_ptr<T> into the argument list for the callback - the referenced object now lives for as long as the callback itself lives. This isn't explicit in the code, and even today, probably 12-15 years after we first realized the anti-pattern, we still do it from time to time.
shared_ptr<int> global;
void func(const shared_ptr<int>& v) {
global.reset();
*v += 1; // ohno
}
func(global);/* XXX the argument might be the same as the global value we're about to modify. API designers, eh? */
I'd also like to inquire as to what nontrivial codebase you've seen that contains no fairly stupid programming. I keep hearing about them, and trying to prove their existence, but so far I've collected more concrete evidence for the existence of bigfoot than I have for the existence of such codebases.
I loathe when someone or some article rants on about their silver bullet solution, or alternatively, that doing some thing is always bad.
I learned a test taking strategy early in my life - when faced with a multiple choice question, if you see "always" or "never", that answer is usually incorrect.
To come back around to this article and similar software development articles - we have many tools in our toolbox, the trick is knowing when to use (or not use) a particular tool. Never throw your tools away.
> 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 which I disagree. Rust also has the concept of const correctness because it is heavily inspired by C++. Moreover, Rust's constness behaves far better than C++. To be specific:
* In Rust, variables are immutable (const) by default, declared by "let". To make a variable mutable, you need to write "let mut", which takes more effort. By contrast, C/C++ variables are mutable by default, but made immutable by adding "const". So in Rust, the safer behavior takes less code.
* The Rust compiler checks for mutable variables that don't need to be mutable (e.g. never reassigned, never calling any mutable method), and gives you warning messages to nudge you to remove the unnecessary "mut". I don't recall C/C++ compilers offering this help.
Rust:
fn reads_myself(&self) { ... }
fn writes_myself(&mut self) { ... }
fn reads_arg(w: &Widget) { ... }
fn writes_arg(w: &mut Widget) { ... }
C++: void readsMyself() const { ... }
void writesMyself() { ... }
void readsArg(const Widget &w) { ... }
void writesArg(Widget &w) { ... }There are a number of really interesting aspects to this family of types, but the one that's relevant here is that it handles situations like this very gracefully. If you want to pass your shared object to a function that won't take ownership (shared or otherwise), it's simple - the type just decays into a raw pointer. So your function can be
DoSomethingWithFoo(IFoo* theFoo);
and you can call it with your smart pointer ISharedRef<IFoo> myFoo = Mso::Make<IFoo>(...);
DoSomethingWithFoo(myFoo);
This is of course a double-edged sword because you have to know that your callee won't try to hang on to that pointer. Even so, I never once in all of my (very strict) code reviews heard anything about the proper way to pass pointers. (and believe me, it's the kind of codebase where you _will_ hear about that kind of thing).It's been a few years so maybe some details are wrong, but the gist of it is correct.
Neat! ...not that I'd ever use it, or recommend its use. Its tradeoffs make perfect sense for Office but probably not for general-purpose applications.
std::shared_ptr<int> x {new int(0)};
{
std::unique_ptr<int> y{x}; // not actually allowed
} // boomI totally agree about the danger of implicit conversion to raw pointers - it breaks the whole point of smart pointers. The reason it makes sense for Office is that huge swathes of code are written in "c++99" style - no exceptions, all functions return HRESULT error codes, and most functions are very explicit about who owns what. By convention, functions accepting pointers almost (?) never hang on to those pointers. Those functions that _do_ take ownership of (or retain an interest in) a pointer argument, explicitly ask for a smart pointer.
Like I said, not really suitable for general use, but darn convenient _within the context of Office_.
Yes, there are times when it's the least bad option, but using a shared_ptr is the C++ equivalent of throwing your hands up and going "screw it, I have no idea who's supposed to own this".
What is "const correctness"?(http://yosefk.com/c++fqa/const.html#fqa-18.1)
Should I try to get things const correct "sooner" or "later"? (http://yosefk.com/c++fqa/const.html#fqa-18.3)
What does "const Fred& x" mean? (http://yosefk.com/c++fqa/const.html#fqa-18.6)
Does "Fred& const x" make any sense?(http://yosefk.com/c++fqa/const.html#fqa-18.7)
What does "Fred const& x" mean?(http://yosefk.com/c++fqa/const.html#fqa-18.8)
What's the relationship between a return-by-reference and a const member function?(http://yosefk.com/c++fqa/const.html#fqa-18.11)
void foo( const Widget& widget );
Called by foo( *widget_ptr );
or foo( widget );
No doubt there are special circumstance by normally foo would not care, should not care, whether the Widget is on the stack or heap, controlled by a dump or smart pointer.> Q1: Yes Q2: - Q3: No Argument: `Widget& widget`
...or `Widget* widget`, which has drawbacks but makes it more clear at the callsite that the argument passed may be modified. The Google style guide prefers this for this reason: https://google.github.io/styleguide/cppguide.html#Reference_...
> Q1: No Q2: No Q3: - Argument: `Widget widget`
Q2 IMO should have an addendum to read "Is it expensive to copy a Widget and if not, will it always be cheap" because there is nothing to stop someone from adding to Widget until it crosses the threshold of "expensive", at which point someone needs to go through and update all the callsites to pass by const ref. Widget's copy cost really needs to be part of its API contract before pass-by-value can be allowed.
in ardour, we use the convention that if a null value is allowed, pass a pointer; if a null value is not allowed, pass a ref.
the idea that Widget& implies something different about the likelihood of modification compared with Widget* is quite foreign to me. this is precisely what Widget const & is for.
The issue is not ambiguity in the declaration, but rather the callsite: it's rarely possible to tell if a parameter is passed as a value, ref, or const ref at a particular callsite without consulting the function declaration. Oftentimes, one reads code without comparing each function call to its declaration, so it's easy to miss instances where objects are passed around by mutable ref rather than the more common const ref.
Using a pointer for the mutable cases resolves this ambiguity by forcing different syntax to be used at the callsite, which reminds:
* code authors that the parameter they're passing can be modified by the function call (they should think carefully about whether this is desired)
* code readers that a function call is potentially modifying its parameter (which can aid in debugging)
But the moment you've called
hello_i_accept_pointers (&myvar)
you're now inside a callstack where the variable is a pointer, and there's no syntax to indicate that you're passing a pointer to the next callee.seriously, even with trusty old emacs and ag(1), the declaration of the called function is a fraction of a second away, no matter where it lives.
It's something that's not easy to realize how useful it is until you actually do it. When working in a large codebase that consistently follows those rules it just makes code reading a breeze, it severely reduces the number of "context switches".
Obviously it's not the only way it can be done but it is a way and the benefits are significant.
Generally, things that are passed by value are de-facto “frozen” and will not have new members added to them, as they’d no longer be “zero cost”. For example, std:string::iterator is “logically” a char * and this will likely never change.
I mean, I guess it's a way to avoid this particular problem (https://godbolt.org/z/8g6HJg), but even without passing shared_ptr by const ref that code still has a far worse code smell, namely the way it mixes manual and automated memory management. After all, the whole point of creating a shared_ptr instance is to take (possibly shared) ownership of a thing.
foo(shared_ptr<T> const &x) {
saved_x = x;
}
and it'll update the refcount exactly once when doing the assignment.And not pointing out that a `foo(const std::shared_ptr<Widget>& widget);` should most likely become a plain `foo(const Widget& widget);` with a `*widget_ptr` dereference at the callsite.
You can solve problems with them but the moment I see them in code it raises flags that make me think someone gave up solving the actual problem at hand and “this seemed to work”.
I’ve seen many an otherwise good looking code base paint itself into corners over object ownership, life cycles and resource reclamation.
In the case of Modern C++, the heavy emphasis on performance plus syntax density, take away the fun parts (to me) and leave one with neither fun synonyms nor just-the-facts simplicity!
Isn't this sort of hair-splitting one reason Theology has a bad name amongst some intellectuals ?
Of course the only sane use case is that that unrelated shared_ptr will definitely outlive that other pointer. But you get the idea, shared_ptr is incredibly flexible.
It’s a shame, because there are some places where C++ is a strict improvement over C, but you have to accept so much more baggage.
Precisely. Whereas the latter debate and complain about whether code is "OOP or not", and think non-OOP is a "bad practice", now we have the same contention around whether code is "pure C++" and dogma that "C with classes" is to be avoided. Hopefully the latter group will also eventually realise the silliness of it all.
I personally use C++ as "C with some extra features" and try to avoid most of the template insanity.
Could you explain how you would improve the shared_ptr syntax? I'm not sure you could make it that much simpler. std::shared_ptr<Something> seems pretty clear to me.
Passing a shared_ptr by const ref is okay if you’re absolutely sure about your (smart) pointer lifetime. I’m never really sure or how things will evolve... Yesterday is history, tomorrow a mystery!
That’s why rust’s lifetimes rock!
What I'm really interested in is whether an argument is allowed to be modified by the function or not. I'm pretty sure the compiler has a better chance of making the right decisions concerning performance. Especially considering how best practices morph as languages/compilers evolve.
Go has the same issues, I'm at the point where I'm mostly passing anything that's not supposed to be modified by value. Notable exceptions are interfaces, which need pointer receivers to work for reasons.