There are some algorithms which are complicated/inefficient/impossible to write without a GC. I suspect this is the major selling point for GC in C++, and can't ever see it being used as a general-purpose allocator.
Why not a general-purpose allocator?
First, We have RAII, and most (including me) seem to believe that this is a superior method of resource management. We like determinism because it makes writing robust, leak-free code a lot simpler and makes performance predictable.
Second, you'll need to place some very un-C++-like restrictions on how you can use memory. For instance, you'd need at least one reachable, un-obfuscated pointer. Obfuscated pointers, as are popular in common tree container libraries (using alignment-guaranteed low bits for color flags) among others, won't be recognizable by the GC.
Related to that, the things which make modern GCs so usable are going to be very difficult to apply to C++ if you support any number of obfuscated pointers. Generational defragmenting GCs are really cool, because allocating is extremely cheap (essentially just incrementing a pointer) and eventually your allocations get compacted into something smaller with improved locality. To do this, objects need to be movable.
To make an object safely movable, the GC needs to be able to update all the pointers to it. It won't be able to find obfuscated ones. This could be accomodated, but wouldn't be pretty (probably a gc_pin
type or similar, used like current std::lock_guard
, which is used whenever you need a raw pointer). Usability would be out the door.
Without making things movable, a GC would be significantly slower and less scalable than what you're used to elsewhere.
Usability reasons (resource management) and efficiency reasons (fast, movable allocations) out of the way, what else is GC good for? Certainly not general-purpose. Enter lock-free algorithms.
Why lock-free?
Lock-free algorithms work by letting an operation under contention go temporarily "out of sync" with the data structure and detecting/correcting this at a later step. One effect of this is that under contention memory might be accessed after it has been deleted. For example, if you have multiple threads competing to pop a node from a LIFO, it is possible for one thread to pop and delete the node before another thread has realized the node was already taken:
Thread A:
- Get pointer to root node.
- Get pointer to next node from root node.
- Suspend
Thread B:
- Get pointer to root node.
- Suspend
Thread A:
- Pop node. (replace root node pointer with next node pointer, if root node pointer hasn't changed since it was read.)
- Delete node.
- Suspend
Thread B:
- Get pointer to next node from our pointer of root node, which is now "out of sync" and was just deleted so instead we crash.
With GC you can avoid the possibility of reading from uncommitted memory because the node would never be deleted while Thread B is referencing it. There are ways around this, such as hazard pointers or catching SEH exceptions on Windows, but these can hurt performance significantly. GC tends to be the most optimal solution here.