8

I have seen several implementations of shared_ptr, for example here. All of them declare ref_count as int*. I don't understand what would we lose if it is simply an int. Thanks!

template <class T>
class shared_ptr {
    T* ptr;
    int* ref_count;

    /**
     * Initializes the ref count used for tracking the usage.
     */
    void initialize_ref_count() {
        if (ref_count != nullptr)
            return;
        try {
            ref_count = new int;
            *ref_count = 1;
        }
        catch (std::bad_alloc& e) {
            std::cerr << "Memory allocation error: " << e.what();
        }
    }
}
havij
  • 1,030
  • 14
  • 29
  • 19
    If it's simply an `int`, it wouldn't be very shared. The whole point of the exercise is that multiple instances of `shared_ptr` that share the same `ptr` **also** share the corresponding reference count. – Igor Tandetnik Aug 29 '17 at 03:40
  • Possible duplicate of [Why are two raw pointers to the managed object needed in std::shared\_ptr implementation?](https://stackoverflow.com/questions/34046070/why-are-two-raw-pointers-to-the-managed-object-needed-in-stdshared-ptr-impleme) – Zan Lynx Aug 29 '17 at 03:47
  • There is a *lot* of crappy code out there. It should definitely be `size_t` rather than `int`. – o11c Aug 29 '17 at 04:14
  • 3
    @o11c why? And I think that's not the point – Passer By Aug 29 '17 at 04:18
  • @PasserBy First off: signed numbers are evil, they act really weird. Second, real-world code *will* create more than 2³² objects. – o11c Aug 29 '17 at 04:36
  • 2
    @o11c You'll find very little real-world code where the same object is shared between more than four billion owners. – molbdnilo Aug 29 '17 at 05:52
  • 2
    @o11c How exactly are signed numbers evil? That real-world code creates more than 2³² objects is even the more reason that picking one over another is just as bad. But tbh I don't think you should be automatically reference counting 2³² objects – Passer By Aug 29 '17 at 06:05
  • @molbdnilo unfortunately, we programmers can't afford to live in the "it usually will work" land. Because it only has to fail once. – o11c Aug 29 '17 at 06:05
  • 4
    It is actually *unsigned* numbers that act "really weird". The modulo 2^n arithmetic is unique to unsigned values, and makes them primarily suitable for bit fields, rather than as actual integers. If you might need the extra capacity provided by the sign bit, then you should almost certainly just use the next larger integer size. – Cody Gray - on strike Aug 29 '17 at 08:19
  • @CodyGray : `size_t` is not only unsigned, it is also longer than `int` (on almost all platforms). It's not the one extra sign bit, it's the 32 extra value bits! – Martin Bonner supports Monica Aug 29 '17 at 08:57
  • @CodyGray Undefined Behavior is weirder than wrapping. – o11c Aug 29 '17 at 17:46

2 Answers2

8

As you can see in the implementation you provided (in your link), when a shared pointer is copy constructed or copy assigned, the pointer to the reference counter (ref_count) is shared between all the instances that manage the same pointer:

    // Copy constructor
    shared_ptr(const shared_ptr& copy) {
        ptr = copy.ptr;
        ref_count = copy.ref_count; // see here
        if (ref_count != nullptr) {
            ++(*ref_count);
        }
    }

    // Assignment operator
    shared_ptr& operator=(const shared_ptr& copy) {
        ptr = copy.ptr;
        ref_count = copy.ref_count; // see here
        if (ref_count != nullptr) {
            ++(*ref_count);
        }
        return *this;
}

In that way, all the instances of that shared pointer, refer to the same memory location to track the ref counter, and the last shared_ptr will be able to know if it needs to do the cleaning (delete the allocated memory):

   ~shared_ptr() {
        --(*ref_count);
        if (*ref_count == 0) {
            delete ref_count;
            ref_count = nullptr;
            delete ptr;
            ptr = nullptr;
        }
    }

Disclaimer

This answer was based in the example provided by the OP for simplicity. A shared_ptr implementation is far more complicated that the one in the example (think about atomicity, race conditions, etc...).

whoan
  • 8,143
  • 4
  • 39
  • 48
  • 4
    FWIW, the `operator=` function is faulty. It should have decremented the `ref_count` of `this` first before assigning `copy.ref_count` to it. – R Sahu Aug 29 '17 at 03:53
  • Thanks for the clarification. I am not assuming the implementation provided by the OP is correct. My only goal was explaining why a pointer was needed. – whoan Aug 29 '17 at 03:58
  • 1
    The complexity you mentioned only exists in `std::shared_ptr`, the concept of reference counting doesn't necessarily include that – Passer By Aug 29 '17 at 04:12
3

First, in other to share something, you need to put it somewhere where others can have access to. As noted by @Igor Tandetnik. So an object of dynamic storage duration would do the job well. An object of a static storage duration with dynamic initialization could do it too, but the object will exist for the remainder of the program, which we do not want.


Secondly, shared_ptr is a bit more complicated than that. A typical shared_ptr would refer to a Control Block. This Control block usually contains:

  • the number of shared references to the object,
  • the object in question or a pointer to the object,
  • and the number of weak references to the Control Block.

For thread safety, the shared reference count and weak reference count is typically held by an atomic type.

EDIT: See @Passer By's comment.

WhiZTiM
  • 21,207
  • 4
  • 43
  • 68