56

As I can see, smart pointers are used extensively in many real-world C++ projects.

Though some kind of smart pointers are obviously beneficial to support RAII and ownership transfers, there is also a trend of using shared pointers by default, as a way of "garbage collection", so that the programmer do not have to think about allocation that much.

Why are shared pointers more popular than integrating a proper garbage collector like Boehm GC? (Or do you agree at all, that they are more popular than actual GCs?)

I know about two advantages of conventional GCs over reference-counting:

  • Conventional GC algorithms has no problem with reference-cycles.
  • Reference-count is generally slower than a proper GC.

What are the reasons for using reference-counting smart pointers?

hyde
  • 3,744
  • 4
  • 25
  • 35
  • 6
    I'd just add a comment that this is a wrong default to use: in most cases, `std::unique_ptr` is sufficient and as such has zero-overhead over raw pointers in terms of run-time performance. By using `std::shared_ptr` everywhere you'd also obscure the ownership semantics, losing one of the major benefits of smart pointers other than automatic resource management -- clear understanding of the intent behind the code. – Matt Aug 14 '13 at 17:32
  • 2
    Sorry but the accepted answer here is completely wrong. Reference counting has higher overheads (a count instead of a mark bit and slower run-time performance), unbounded pause times when decrements avalanche and no more complex that, say, Cheney semi-space. – J D Jan 26 '16 at 20:45

5 Answers5

60

Some advantages of reference counting over garbage collection:

  1. Low overhead. Garbage collectors can be quite intrusive (e.g. making your program freeze up at unpredictable times while a garbage collection cycle processes) and quite memory-intensive (e.g. your process's memory footprint unnecessarily grows to many megabytes before garbage-collection finally kicks in)

  2. More predictable behavior. With reference counting, you are guaranteed that your object will be freed the instant the last reference to it goes away. With garbage collection, on the other hand, your object will be freed "sometime", when the system gets around to it. For RAM this isn't usually a big problem on desktops or lightly loaded servers, but for other resources (e.g. file handles) you often need them be closed ASAP to avoid potential conflicts later on.

  3. Simpler. Reference counting can be explained in a few minutes, and implemented in an hour or two. Garbage collectors, especially ones with decent performance, are extremely complex and not many people understand them.

  4. Standard. C++ includes reference counting (via shared_ptr) and friends in the STL, which means that most C++ programmers are familiar with it and most C++ code will work with it. There isn't any standard C++ garbage collector, though, which means that you have to choose one and hope it works well for your use case -- and if it doesn't, it's your problem to fix, not the language's.

As for the alleged downsides of reference counting -- not detecting cycles is an issue, but one that I've never personally ran into in the last ten years of using reference counting. Most data structures are naturally acyclical, and if you do come across a situation where you need cyclical references (e.g. parent pointer in a tree node) you can just use a weak_ptr or a raw C pointer for the "backwards direction". As long as you are aware of the potential problem when you're designing your data structures, it's a non-issue.

As for performance, I've never had a problem with the performance of reference counting. I have had problems with the performance of garbage collection, in particular the random freeze-ups that GC can incur, to which the only solution ("don't allocate objects") might as well be rephrased as "don't use GC".

Jeremy Friesner
  • 1,091
  • 8
  • 11
  • 18
    Naïve reference-counting implementations typically get *much* lower throughput than production GCs (30–40%) at the expense of latency. The gap can be closed with optimisations such as using fewer bits for the refcount, and avoiding tracking objects until they escape—C++ does this last naturally if you mainly `make_shared` when returning. Still, latency tends to be the bigger problem in realtime applications, but throughput is more important generally, which is why tracing GCs are so widely used. I wouldn’t be so quick to speak badly of them. – Jon Purdy Aug 14 '13 at 06:13
  • 4
    I'd quibble 'simpler': simpler in terms of the *total amount of implementation required* yes, but not simpler for the code that *uses* it: compare telling someone how to use RC ('do *this* when creating objects and *this* when destroying them') to how to (naively, which is often enough) use GC ('...'). – AakashM Aug 14 '13 at 08:08
  • 2
    (although I'm probably hideously unaware of how easy smart pointers are to use in modern versions of C-ish languages) – AakashM Aug 14 '13 at 09:00
  • 1
    C++ is incompatible with garbage collector. The C++11 specification undefined some behaviour to make boehm gc compliant, but IIRC there was some argument that it's still missing some bits. Does not explain why some other interpreters like perl or python hang on to the reference counting collectors (both have heap walk to deal with cycles _too_). – Jan Hudec Aug 14 '13 at 12:52
  • @JanHudec Python (by which you mean CPython, I assume) hangs onto refcounting because every piece of code even slightly related to its API, including millions of lines of third party code, is completely dependent on refcounting and non-moving objects. –  Aug 14 '13 at 13:41
  • @JanHudec: Could a GC operate 100% correctly in C++ if the only references to GC managed items were themselves ref-counted or non-shareable objects [so that the GC would know whenever any legitimate reference to a GC-managed object came into existence or disappeared]? – supercat Dec 24 '13 at 23:00
  • 1
    @supercat: Standard pointers can be reinterpret-casted, which prevents the collector from ever being sure what is a pointer. You'd need special kind of pointers and special rules for them and than it would no longer be useful with existing code. – Jan Hudec Dec 28 '13 at 19:39
  • @JanHudec: The constructor for a `gcRefPtr` object would add the object to the garbage-collector's private list of extant references, and the destructor would remove it. The GC's internal structures wouldn't be exposed to the outside world, so nobody outside could reinterpret cast them, and the GC wouldn't care how anything in the outside world stored any `gcRefPtr` objects. If code outside never calls the destructor on a `gcRefPtr`, the GC would hold the object (and anything to which it internally holds a reference) forever. If code outside tries to do anything wiht destructed `gcRefPtr`... – supercat Dec 28 '13 at 19:49
  • @JanHudec: ...execution would enter the realm of nasal demons. Both of those are no different from the issues associated with normal pointers. The idea, however, would be that given `{ gcRefPtr p1,p2; ... p1=p2; ...}` entering the block would create two gcRefPtr objects; the assignment would not destroy p1 but rather copy the reference from p2 to p1, and the end of the block would destroy both p1 and p2. Seldom should one need to create multiple non-temporary pointers to the same `gcRefPtr` object. Instead, one would create a separate `gcRefPtr` each place a persistent ref was needed. – supercat Dec 28 '13 at 20:02
  • @JanHudec: I'm not really experienced with the tricky aspects of C++, so it's entirely possible I'm missing some pretty big issues (most likely difficulty with regard to compatibility of things inside and outside the GC universe) but trying to use such a GC pattern in C++ seems much nicer than trying to do so in C. – supercat Dec 28 '13 at 20:04
  • @supercat: Maintaining a list at runtime would be more expensive than reference counting, therefore it would not have any advantage over `shared_ptr`. Most probably it would be a lot more expensive. – Jan Hudec Dec 28 '13 at 21:47
  • @supercat: Yes, you can get a special type of pointers to work. It's basically what Microsoft did in Managed C++. And note, that they would _not_ have to keep a list of anything at runtime; compile-time support is sufficient. But it would not be very useful with all the existing code using plain old pointers. – Jan Hudec Dec 28 '13 at 21:49
  • @JanHudec: If the compiler produces the proper metadata to indicate where references are stored at all times during program execution one can avoid the need to create and destroy references at runtime. That requires, however, that one have a compiler that produces all the required information. It's certainly possible to do GC even in "straight" C if one knows how all references will be stored; I would think C++ should be able to do even better. – supercat Dec 29 '13 at 01:20
  • 1
    "Garbage collectors can be quite intrusive (e.g. making your program freeze up at unpredictable times while a garbage collection cycle processes) and quite memory-intensive (e.g. your process's memory footprint unnecessarily grows to many megabytes before garbage-collection finally kicks in)". Garbage collectors can be real time and your latter observation is not applicable to most garbage collectors. For example, OCaml is an obvious counter example. – J D Feb 20 '15 at 14:34
  • 4
    "With reference counting, you are guaranteed that your object will be freed the instant the last reference to it goes away". That is a common misconception. http://flyingfrogblog.blogspot.co.uk/2013/10/memory-management-myths-promptness.html – J D Feb 20 '15 at 14:34
  • "Reference counting can be explained in a few minutes, and implemented in an hour or two. Garbage collectors, especially ones with decent performance, are extremely complex and not many people understand them." Here is another counter example: http://stackoverflow.com/a/11118079/13924 – J D Feb 20 '15 at 14:36
  • 2
    It's logical fallacy to imply that a single example could ever disprove a "can-be" relationship. – MickLH Mar 01 '15 at 15:51
  • 5
    @JonHarrop: That blog-post is horribly wrong-headed. You should also read *all* the comments, especially the last one. – Deduplicator Jan 27 '16 at 09:03
  • @Deduplicator: "That blog-post is horribly wrong-headed". That blog post gives code that proves your belief wrong. – J D Jan 27 '16 at 12:38
  • 2
    @JonHarrop: No. It just demonstrates a mis-understanding of scopes and lifetimes. And what optimization really is behind allowing that F# object to be collected, when it actually happens. – Deduplicator Jan 27 '16 at 12:42
  • 1
    @Deduplicator: I'm afraid there is no such misunderstanding and no such "optimisation". If you can be specific then I can explain why but I suspect you now know you're wrong. – J D Jan 27 '16 at 12:52
  • 3
    @JonHarrop: Yes, there is. He doesn't understand that the lifetime is the full scope which goes up to the closing brace. And the optimisation in F# which according to the comments only sometimes works is ending the lifetime earlier, if the variable is not used again. Which naturally has its own perils. – Deduplicator Jan 27 '16 at 12:56
  • 2
    @Deduplicator: "He doesn't understand that the lifetime is the full scope which goes up to the closing brace". I understand that perfectly well. Indeed, that is the whole point of my blog post: C++ `shared_ptr` keeps floating garbage around to the end of scope contrary to common folklore that it collects at the earliest opportunity. – J D Jan 27 '16 at 12:59
  • 3
    @Deduplicator: "And the optimisation in F# which according to the comments only sometimes works is ending the lifetime earlier, if the variable is not used again. Which naturally has its own perils". That is called liveness analysis and it is done by all production compilers not just F#. It is nothing "special" as he claims. C++ compilers were doing this long before F# existed. They do not spill everything to the stack for the entire scope as he claims. He obviously has no idea how compilers work... – J D Jan 27 '16 at 13:01
  • 2
    "For RAM this isn't usually a big problem on desktops or lightly loaded servers, but for other resources (e.g. file handles) you often need to be closed ASAP even there", GC is automatic memory management, not for any other kind of resource. – coredump Aug 09 '16 at 17:58
  • 1
    @coredump Good point -- I should have added one more reason why reference counting is popular: it can be used for other kinds of resources besides memory. – Jeremy Friesner Dec 03 '16 at 04:05
27

To get good performance out of a GC, the GC needs to be able to move objects in memory. In a language like C++ where you can interact directly with memory locations, this is pretty much impossible. (Microsoft C++/CLR doesn't count because it introduces new syntax for GC-managed pointers and is thus effectively a different language.)

The Boehm GC, while a nifty idea, is actually the worst of both worlds: you need a malloc() that is slower than a good GC, and so you lose the deterministic allocation/deallocation behavior without the corresponding performance boost of a generational GC. Plus it is by necessity conservative, so it won't necessarily collect all your garbage anyway.

A good, well-tuned GC can be a great thing. But in a language like C++, the gains are minimal and the costs are often just not worth it.

It will be interesting to see, however, as C++11 becomes more popular, whether lambdas and capture semantics start to lead the C++ community towards the same kinds of allocation and object lifetime problems that caused the Lisp community to invent GCs in the first place.

See also my answer to a related question over on StackOverflow.

Daniel Pryden
  • 3,268
  • 1
  • 21
  • 21
  • 6
    RE the Boehm GC, I've occasionally wondered how much *it personally* is responsible for the traditional aversion to GC among C and C++ programmers simply by providing a bad first impression of the technology in general. – Alex Celeste Feb 19 '15 at 02:06
  • @Leushenko Well said. A case in point is this question, where Boehm gc is called a "proper" gc, ignoring the fact that it's slow and practically guaranteed to leak. I found this question while researching whether someone implemented python-style cycle breaker for shared_ptr, which sounds like a worthwhile goal for a c++ implementation. – user4815162342 Dec 17 '15 at 18:12
4

As I can see, smart pointers are used extensively in many real-world C++ projects.

True but, objectively, the vast majority of code is now written in modern languages with tracing garbage collectors.

Though some kind of smart pointers are obviously beneficial to support RAII and ownership transfers, there is also a trend of using shared pointers by default, as a way of "garbage collection", so that the programmer do not have to think about allocation that much.

That's a bad idea because you still need to worry about cycles.

Why are shared pointers more popular than integrating a proper garbage collector like Boehm GC? (Or do you agree at all, that they are more popular than actual GCs?)

Oh wow, there are so many things wrong with your line of thinking:

  1. Boehm's GC is not a "proper" GC in any sense of the word. It is truly awful. It is conservative so it leaks and is inefficient by design. See: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Shared pointers are, objectively, nowhere near as popular as GC because the vast majority of developers are using GC'd languages now and have no need of shared pointers. Just look at Java and Javascript in the job market compared to C++.

  3. You appear to be restricting consideration to C++ because, I assume, you think that GC is a tangential issue. It isn't (the only way to get a decent GC is to design the language and VM for it from the beginning) so you are introducing selection bias. People who really want proper garbage collection don't stick with C++.

What are the reasons for using reference-counting smart pointers?

You are restricted to C++ but wish you had automatic memory management.

J D
  • 2,332
  • 24
  • 17
  • 7
    Um, it's a question tagged [tag:c++] which talks about C++ features. Clearly, any general statements are talking about *within* C++ code, not the entirety of programming. So however "objectively" garbage collection may be in use outside of the C++ world, that is ultimately irrelevant to the question at hand. – Nicol Bolas Jan 27 '16 at 02:57
  • 2
    Your last line is patently wrong: You are in C++ and glad you aren't forced to deal with GC and it's delayed freeing of resources. There's a reason Apple doesn't like GC, and the most important guideline for GC'd languages is: Don't create any garbage unless you have gobs of idle resources or cannot help it. – Deduplicator Jan 27 '16 at 09:09
  • @Deduplicator: "GC and it's delayed freeing of resources". All techniques produce floating garbage. Avoiding it requires you to solve the halting problem. – J D Jan 27 '16 at 12:36
  • @Deduplicator: "There's a reason Apple doesn't like GC". Yes, they are incompetent. There's a reason why Android uses GC and has more market share. – J D Jan 27 '16 at 12:36
  • @Deduplicator: "Don't create any garbage unless you have gobs of idle resources or cannot help it". I have never seen any concrete evidence that scope-based reference counting requires less memory than tracing garbage collection. – J D Jan 27 '16 at 12:57
  • 3
    @JonHarrop: So, compare equivalent small programs with and without GC, which aren't explicitly picked to play to either side's advantage. Which one do you expect to need more memory? – Deduplicator Jan 27 '16 at 13:02
  • 1
    @Deduplicator: I can envisage programs that give either outcome. Reference counting would outperform tracing GC when the program is designed to keep heap allocate memory until it survives the nursery (e.g. a queue of lists) because that is pathological performance for a generational GC and would generate the most floating garbage. Tracing garbage collection would require less memory than scope-based reference counting when there are many small objects and lifetimes are short but not statically well known so something like a logic program using purely functional data structures. – J D Jan 27 '16 at 13:16
  • @Deduplicator: "with and without GC". You meant with tracing GC and with reference counting, right? – J D Jan 27 '16 at 13:17
  • 3
    @JonHarrop: I meant with GC (tracing or whatever) and RAII if you speak C++. Which includes reference-counting, but only where it's useful. Or you could compare with a Swift program. – Deduplicator Jan 27 '16 at 13:43
  • @Deduplicator: I just benchmarked Rust vs F# on a HashSet benchmark and found F# to be faster in all cases. I'll take a second look from the point of view of memory consumption. – J D Apr 04 '16 at 14:10
  • @JonHarrop And you are good with rust, and used optimization? Don't know how good the rust-compiler optimizes though, probably nowhere near a C++ compiler yet. – Deduplicator Apr 05 '16 at 08:09
  • @Deduplicator: No, I am a complete Rust noob. However, I did ask the community for feedback. https://www.reddit.com/r/rust/comments/4dd5yl/rust_vs_f_hashset_benchmark/ – J D Apr 05 '16 at 21:08
  • @Deduplicator: "Don't know how good the rust-compiler optimizes though, probably nowhere near a C++ compiler yet". Actually C++ was slower than both F# and Rust on my benchmark. Rust uses the LLVM which is the back-end of several major C++ compilers so it generates excellent code. Rust is also able to drive LLVM better by design. – J D Apr 05 '16 at 21:09
3

In MacOS X and iOS, and with developers using Objective-C or Swift, reference counting is popular because it is handled automatically, and the use of garbage collecting has considerably decreased since Apple doesn't support it anymore (I am told that apps using garbage collection will break in the next MacOS X version, and garbage collection was never implemented in iOS). I actually seriously doubt that there was ever much software using garbage collection when it was available.

The reason for getting rid of garbage collection: It never worked reliably in a C-style environment where pointers could "escape" to areas not accessible by the garbage collector. Apple strongly believed and believes that reference counting is faster. (You can make any claims here about relative speed, but nobody has been able to convince Apple). And in the end, nobody used garbage collection.

First thing that any MacOS X or iOS developer learns is how to handle reference cycles, so that's not a problem for a real developer.

gnasher729
  • 42,090
  • 4
  • 59
  • 119
  • The way I understand it, it wasn't that it's a C-like environment which decided things but that GC is indeterministic and needs much more memory in order to have acceptable performance, and outside server/desktop that's always a bit scarce. – Deduplicator Jan 27 '16 at 09:19
  • Debugging why the garbage collector destroyed an object that I was still using (leading to a crash) decided it _for me_ :-) – gnasher729 Jan 27 '16 at 11:27
  • Oh yes, that would do it too. Did you in the end find out why? – Deduplicator Jan 27 '16 at 11:29
  • Yes, it was one of many Unix functions where you pass a void* as a "context" which is then given back to you in a callback function; the void* was really an Objective-C object, and the garbage collector didn't realise the object was stashed away in the Unix call. Callback is called, casts void* to Object*, kaboom! – gnasher729 Jan 27 '16 at 11:35
2

The biggest disadvantage of garbage collection in C++ is, that it's plain impossible to get right:

  • In C++, pointers do not live in their own walled community, they are mixed with other data. As such, you can't distinguish a pointer from other data that just happens to have a bit pattern that can be interpreted as a valid pointer.

    Consequence: Any C++ garbage collector will leak objects that should be collected.

  • In C++, you can do pointer arithmetic to derive pointers. As such, if you don't find a pointer to the start of a block, that does not mean that that block can't be referenced.

    Consequence: Any C++ garbage collector has to take these adjustments into account, treating any bit sequence that happens to point anywhere within a block, including right after the end of it, as a valid pointer that references the block.

    Note: No C++ garbage collector can handle code with tricks like these:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;
    

    True, this invokes undefined behavior. But some existing code is more clever than is good for it, and it may trigger preliminary deallocation by a garbage collector.

  • 2
    "*they are mixed with other data.*" It's not so much that they're "mixed" with other data. It's easy to use the C++ type system to see what's a pointer and what is not. The problem is that pointers frequently *become* other data. Hiding a pointer in an integer is an unfortunately common tool for many C-style APIs. – Nicol Bolas Jan 27 '16 at 02:57
  • 1
    You don't even need undefined behaviour to screw up a garbage collector in c++. You could, for example, serialize a pointer to a file and read it in later. In the meanwhile, your process may not contain that pointer anywhere in its address space, so the garbage collector could collect that object, and then when you deserialize the pointer... Whoops. – Bwmat Jan 27 '16 at 04:36
  • @Bwmat "Even"? Writing pointers to a file like that seems a bit... far-fetched. Anyway, same serious problem plagues pointers to stack objects, they may be gone when you read the pointer back from file elsewhere in the code! Deserializing invalid pointer value *is* undefined behavior, don't do that. – hyde Jan 27 '16 at 06:03
  • If course, you would need to be careful if you are doing something like that. It was meant to be an example that, in general, a garbage collector can't work 'properly' in all cases in c++ (without changing the language) – Bwmat Jan 27 '16 at 06:06
  • @Bwmat Which is precisely my point. The pointer arithmetic is just an example to show that garbage collectors may miss stuff in C++, leading to preliminary deallocation of objects. – cmaster - reinstate monica Jan 27 '16 at 06:17
  • @NicolBotas No, you can't use C++'s type system: It is perfectly legal to cast a `type**` to a `char*`, copy `sizeof(type*)` bytes over to another place, and cast that back to a pointer. In that case the destination was not typed as a pointer when it was written, even though there is no undefined behavior at any place. A garbage collector cannot be sure that data written as `char` is not a pointer. – cmaster - reinstate monica Jan 27 '16 at 06:22
  • You might want to read `[basic.stc.dynamic.safety]`: Safely-derived pointers. That's one of the sections of the standard relevant to GC. – Deduplicator Jan 27 '16 at 09:06
  • If you write array += 7; then you still have defined behaviour, but the pointer can now point to another array, so array could be deallocated by a garbage collector without undefined behaviour. – gnasher729 Jan 27 '16 at 11:37
  • 1
    @gnasher729: Ehm, no? Past-end-pointers are perfectly fine? – Deduplicator Jan 27 '16 at 12:40
  • If you have two consecutive arrays in memory, then the perfectly legal pointer past the end of the first array may be identical to a pointer to the start of the second array by coincidence. So the garbage collector will likely think it points to the second array and keeps it alive, while really it points to the first array. – gnasher729 Jan 27 '16 at 16:10
  • @gnasher729 The garbage collector would have to treat such a pointer as pointing to *both* arrays. Because, as Deduplicator said, past-the-end pointers/iterators are a really common, perfectly legal thing. – cmaster - reinstate monica Jan 28 '16 at 06:30