57

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?

2
  • 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, 2013 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, 2016 at 20:45

5 Answers 5

61

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".

26
  • 19
    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, 2013 at 6: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, 2013 at 8:08
  • 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. flyingfrogblog.blogspot.co.uk/2013/10/…
    – J D
    Feb 20, 2015 at 14:34
  • 5
    @JonHarrop: That blog-post is horribly wrong-headed. You should also read all the comments, especially the last one. Jan 27, 2016 at 9:03
  • 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. Jan 27, 2016 at 12:56
28

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.

2
  • 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. Feb 19, 2015 at 2: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. Dec 17, 2015 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.

13
  • 7
    Um, it's a question tagged 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. Jan 27, 2016 at 2: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. Jan 27, 2016 at 9:09
  • 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? Jan 27, 2016 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, 2016 at 13:16
  • 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. Jan 27, 2016 at 13:43
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.

4
  • 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. Jan 27, 2016 at 9: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, 2016 at 11:27
  • Oh yes, that would do it too. Did you in the end find out why? Jan 27, 2016 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, 2016 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.

11
  • 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. Jan 27, 2016 at 2: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, 2016 at 4: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, 2016 at 6: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, 2016 at 6:06
  • 1
    @gnasher729: Ehm, no? Past-end-pointers are perfectly fine? Jan 27, 2016 at 12:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.