The idea behind this is mainly educational but I might even consider using it in reality if turns out to be good. Here's my first try at implementing smart pointers:

template<typename T>
class smart_pointer{
    T* pointer;
    std::size_t *refs;

    void clear(){
        if (!--*refs){
            delete pointer;
            delete refs;
        }
    }

public:
    smart_pointer(T* p = NULL)
        : pointer(p), refs(new std::size_t(1))
    {}
    smart_pointer(const smart_pointer<T>& other)
        : pointer(other.pointer), refs(other.refs)
    {
        ++*refs;
    }
    ~smart_pointer(){
        clear();
    }

    smart_pointer<T>& operator=(const smart_pointer<T>& other){
        if (this != &other){
            clear();

            pointer = other.pointer;
            refs    = other.refs;
            ++*refs;
        }
        return *this;
    }

    smart_pointer<T>& operator=(T* p){
        if (pointer != p){
            pointer = p;
            *refs = 1;
        }
        return *this;
    }

    T& operator*(){
        return *pointer;
    }

    const T& operator*() const{
        return *pointer;
    }

    T* operator->(){
        return pointer;
    }

    const T* operator->() const{
        return pointer;
    }

    std::size_t getCounts(){
        return *refs;
    }
};

I have tested this under valrind and its clean. Also, to see how much the "smartness" makes things slower, I did the following test:

struct foo{
    int a;
};

template<typename pointer_t>
class bar{
    pointer_t f_;

public:
    bar(foo *f)
        :f_(f)
    {}

    void set(int a){
        f_->a = a;
    }
};

int main()
{
    foo *f = new foo;

    typedef smart_pointer<foo> ptr_t;
//    typedef boost::shared_ptr<foo> ptr_t;
//    typedef foo* ptr_t;

    bar<ptr_t> b(f);
    for (unsigned int i = 0; i<300000000; ++i)
        b.set(i);

//    delete f;
    return 0;
}

Here is some timing between my implementation, boost, and raw pointers: (code compiled with clang++ -O3)

typedef smart_pointer<foo> ptr_t;

real   0m0.006s
user   0m0.001s
sys    0m0.002s

typedef boost::shared_ptr<foo> ptr_t;

real    0m0.336s
user    0m0.332s
sys     0m0.002s

typedef foo* ptr_t;

real    0m0.006s
user    0m0.002s
sys     0m0.003s

My implementation seems to be running almost as fast as raw pointers, which I think is a good sign. What worries me here is why Boost is running slower. Have I missed something in my implementation that is important and I might get into trouble for later?

  • I have a question regarding incrementing the count. what if the smart pointer sp was originally pointed to something else, and you do a sp=sp1. In this case you need to decrease the old count first. – Peiti Peter Li Mar 27 '15 at 7:58
up vote 30 down vote accepted

So the idea behind this is mainly educational...

Cool. Always good to try and understand how things work under the covers.

...but I might even consider using it in reality if turns out to be good.

Please rethink this. Smart pointer implementations are incredibly difficult to get right. Scott Myers, of Effective C++ fame, famously tried to implement a shared_ptr. After feedback from something like 10 iterations, it was still wrong.

Let's go through a few things here.

smart_pointer(T* p = NULL)
  : pointer(p), refs(new std::size_t(1))

This constructor should be explicit to avoid implicit type conversions through construction.

Your operator=(T* p) leaks. Here's a small example:

int main()
{
    int *x = new int(5);
    {
        smart_pointer<int> sp(x);
        sp = new int(6);
    } // sp destroyed here 

    std::cout << *x << "\n"; // This should have been deleted but wasn't
}

To convince yourself that this is the case, modify your clear() method:

void clear(){
    if (!--*refs){
        std::cout << "deleteting " << *pointer << " at " << pointer << "\n";
        delete pointer;
        delete refs;
    }
}

This will print out "deleting 6 at <some memory address>".

Here's just a small rundown of what boost::shared_ptr (or std::shared_ptr) provides that is missing here:

  • Constructors allowing a Deleter, which is used instead of a raw delete.
  • Constructor allowing construction from <template Tp1> shared_ptr(Tp1 * ...) where Tp1 is convertible to type T.
  • Copy constructor allowing construction from a shared_ptr with convertible template type <T1>.
  • Copy constructor taking weak_ptr, unique_ptr.
  • Move constructor and assignment operator.
  • Usage of nothrow where it is required to be.
  • reset, which will replace the currently managed object.
  • All the non-member operators like operator<, operator==, etc that allow containers and algorithms to work correctly. For example, std::set will not work correctly with your pointer class, neither will things like std::sort.
  • A specialization of std::swap.
  • A specialization of std::hash. Your pointer class won't work correctly with std::unordered_set or std::unordered_map.
  • explicit operator bool conversion for nullptr tests, so that one can write if (<some_shared_ptr>) { ... }.
  • Utility functions like std::make_shared which you should pretty much always use when creating a shared_ptr.

The big pain point, and the reason why boost::shared_ptr or std::shared_ptr will be slower is the fact that they are thread-safe. Your smart_ptr when accessed by multiple threads could very easily delete the pointer it holds before another class is done with it, leading to dereferencing of a deleted object, and thus undefined behaviour. Furthermore, all of the atomic operations are specialized for shared_ptr.

The above is not an exhaustive list.

I don't write this to discourage you, but merely to try and reinforce the fact that writing something like this is a lot of work. It is very technical, and really, really easy to get wrong. In terms of the standard library, shared_ptr may be one of the most difficult things to write correctly.

  • 1
    I think we should hold an "everyone write a shared pointer day". – user1095108 Sep 5 '13 at 9:05
  • 4
    I would like to add that OP's implementation allocates the reference counter on the heap separately from the subject data. This can (if used very frequently) be a source of memory fragmentation, it is generally good to avoid small heap allocations not to mention the overhead of allocating 4 bytes is like 32 bytes or something. Also if the subject data and reference counter would be allocated together in a struct one would get better data locality and improved cache behavior. These two reasons are the reasons why std::make_shared<>() exists and is preferred for creation of smart pointers. – Emily L. Feb 3 '14 at 17:10

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.