2

despite the ocean of smart pointer questions out there, I seem to be stuck with one more. I am trying to implement a ref counted smart pointer, but when I try it in the following case, the ref count is wrong. The comments are what I think should be the correct ref counts.

Sptr<B> bp1(new B);       // obj1: ref count = 1
Sptr<B> bp2 = bp1;        // obj1: ref count = 2
bp2 = new B;              // obj1: ref count = 1, obj2: rec count = 1 **problem**

In my implementation, my obj2 ref count is 2, because of this code:

protected:
    void retain() { 
        ++(*_rc);
        std::cout << "retained, rc: " << *_rc << std::endl;
    }
    void release() {
        --(*_rc);
        std::cout << "released, rc: " << *_rc << std::endl;
        if (*_rc == 0) {
            std::cout << "rc = 0, deleting obj" << std::endl;
            delete _ptr;
            _ptr = 0;
            delete _rc;
            _rc = 0;
        }
    }

private:
    T *_ptr;
    int *_rc;

    // Delegate private copy constructor
    template <typename U>
    Sptr(const Sptr<U> *p) : _ptr(p->get()), _rc(p->rc()) {
        if (p->get() != 0) retain();
    }

    // Delegate private assignment operator
    template <typename U>
    Sptr<T> &operator=(const Sptr<U> *p) {
        if (_ptr != 0) release();
        _ptr = p->get();
        _rc = p->rc();
        if (_ptr != 0) retain();
        return *this;
    }

public:
    Sptr() : _ptr(0) {}
    template <typename U>
    Sptr(U *p) : _ptr(p) { _rc = new int(1); }

    // Normal and template copy constructors both delegate to private
    Sptr(const Sptr &o) : Sptr(&o) {
        std::cout << "non-templated copy ctor" << std::endl;
    }
    template <typename U>
    Sptr(const Sptr<U> &o) : Sptr(&o) {
        std::cout << "templated copy ctor" << std::endl;
    }


    // Normal and template assignment operator
    Sptr &operator=(const Sptr &o) {
        std::cout << "non-templated assignment operator" << std::endl;
        return operator=(&o);
    }
    template <typename U>
    Sptr<T> &operator=(const Sptr<U> &o) {
        std::cout << "templated assignment operator" << std::endl;          
        return operator=(&o);
    }
    // Assignment operator for assigning to void or 0
    void operator=(int) {
        if (_ptr != 0) release();
        _ptr = 0;
        _rc = 0;
    }

The constructor is initialized with a ref count = 1, but in my assignment operator, I am retaining the object, making the ref count = 2. But it should only be 1 in this case, because bp2 = new B is only one pointer to that object. I've looked over various smart pointer implementation examples, and I can't seem to figure out how they deal with this case that I'm having trouble with.

Thanks for your time!

James Kuang
  • 10,710
  • 4
  • 28
  • 38

3 Answers3

4

The assignment operator is riddled with small errors and unnecessary complex anyway. For example, when you assign Sptr<T> to itself it will have funny effects. Most manually written assignment operators should look like this:

T& T::operator= (T const& other) {
    T(other).swap(*this);
    return *this;
}

... or

T& T::operator= (T other) {
    other.swap(*this);
    return *this;
}

Once stateful allocators enter the game things change a bit but we can ignore this detail here. The main idea is to leverage the existing work done for the copy constructor and the destructor. Note, that this approach also works if the right hand side isn't T, i.e., you can still leverage a corresponding constructor, the destructor, and swap(). The only potentially additional work is desirable anyway, and trivial to implement: the swap() member. In your case that is very simple, too:

template <typename T>
void Sptr<T>::swap(Sptr<T>& other) {
    std::swap(this->_ptr, other._ptr);
    std::swap(this->_rc,  other._rc);
}

Just a note on your assignment operator taking an int: This is a very bad idea! To reset the pointer, you should probably better have a reset() method. In C++ 2011 you could reasonably have a method taking a std::nullptr_t for this purpose, though.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • Thanks for your elaborate response! I am having trouble understanding how using std::swap will assign one ptr to the other. Will it not just swap the addresses? – James Kuang Nov 17 '12 at 22:19
  • 1
    `std::swap()` just, well, swaps its arguments: `void std::swap(T& o0, T& o1) { T tmp(o0); o0 = o1; o1 = tmp; }`. Normally, you'd `swap()` objects using their respective (namespace-level) `swap()` function, falling back go `std::swap()`, i.e., something like this: `using std::swap; swap(o0, o1);`. In your case you can use `std::swap()` directly because there cannot be any overload for built-in types. – Dietmar Kühl Nov 17 '12 at 22:33
  • OK thanks, and as for the reset() comment, I have this now: void reset() { release(); _ptr = 0; _rc = 0; } – James Kuang Nov 17 '12 at 22:41
2

Define an assignment operator for the raw pointer type. Otherwise, it will construct a new smart pointer with ref count 1, which you will then increase to 2.

Example:

template <typename U>
Sptr<T> &operator=(U *p) 
{
    if (_ptr != 0)
    {
      _ptr = p;
      _rc = new int(1);
    }
    return *this;
}

This stuff is notoriously tricky, and there are probably more bugs waiting. I would make the constructor explicit to prevent other unintentional constructions.

Jason
  • 1,612
  • 16
  • 23
1

From what I see of your code, you need a proper destructor in your smart pointer class to call release and fix the counter. You need at least that modification for your counter to work correctly.

I didn't see a Sptr(T *) constructor in your code, did you omit it?

As a side note, I would probably use a std::pair to store the counter and the T pointer, but your data layout works as it is. As another answer pointed it out, you should pay attention to the self assignment case though.

Here's a minimal implementation following your data layout and interface choices:

#include <iostream>
#include <string>

using namespace std;

template <typename T>
class Sptr {

protected:
  T *_ptr;
  int *_rc;

  virtual void retain() {
    if (_rc)   // pointing to something
      ++(*_rc);
    clog << "retain : " << *_rc << endl;
  }
  virtual void release() {
    if (_rc) {
      --(*_rc);
      clog << "release : " << *_rc << endl;
      if (*_rc == 0) {
        delete _ptr;
        _ptr = NULL;
        delete _rc;
        _rc = NULL;
      }
    }
  }

public:
  Sptr() : _ptr(NULL), _rc(NULL) {} // no reference

  virtual ~Sptr() { release(); }  // drop the reference held by this

  Sptr(T *p): _ptr(p) {  // new independent pointer
     _rc = new int(0); 
     retain();
  }

  virtual Sptr<T> &operator=(T *p) {
    release();
    _ptr = p;
    _rc = new int(0);
    retain();
    return *this;
  }

  Sptr(Sptr<T> &o) : _ptr(o._ptr), _rc(o._rc) {
    retain();
  }

  virtual Sptr<T> &operator=(Sptr<T> &o) {
    if (_rc != o._rc){  // different shared pointer
      release();
      _ptr = o._ptr;
      _rc = o._rc;
      retain();
    }
    return *this;
  }
};

int main(){
  int *i = new int(5);

  Sptr<int> sptr1(i);
  Sptr<int> sptr2(i);
  Sptr<int> sptr3;

  sptr1 = sptr1;
  sptr2 = sptr1;
  sptr3 = sptr1;

}

_rc is the indicator in your class that a pointer is shared with another instance or not.

For instance, in this code:

 B *b = new B;
 Sptr<B> sptr1(b);
 Sptr<B> sptr2(b);

sptr1 and sptr2 are not sharing the pointer, because assignments were done separately, and thus the _rc should be different, which is effectively the case in my small implementation.

didierc
  • 14,572
  • 3
  • 32
  • 52
  • I don't think in my test code, any objects have reached the point where they would call their destructor yet. I have yet to implement the destructors, but I think I am having issues aside from that right now. Thanks! – James Kuang Nov 17 '12 at 22:21
  • I added destructor as you suggested, and interestingly it is called during the bp2 = new B line of code. I previously thought it would only get called at end of program. It does fix the problem by incrementing _rc then decrementing it right after, but I can't understand why it is so. I thought the new returns a raw pointer of type U, but my Sptr's destructor is called? – James Kuang Nov 17 '12 at 23:01
  • A side question, what is the reasoning/advantage behind using std::pair to store the counter and pointer versus what I am doing above? – James Kuang Nov 18 '12 at 20:25
  • 1
    You have to copy both values when doing an assignment, whereas with a pair, you would only have to do one copy instruction. Besides, when _ptr changes, _rc has to change too. – didierc Nov 18 '12 at 21:41
  • 1
    I modified my answer to add a (tested) sample implementation. see [there (http://ideone.com/dSp4Rm)](http://ideone.com/dSp4Rm) – didierc Nov 18 '12 at 21:43
  • 1
    I didn't take in account the fact that the `T *` value shouldn't change (constructor & assignment operators should probably take a `T const *`, see the other answers). – didierc Nov 18 '12 at 21:47