thread-safe copy assignment operator for smart pointer class

773 views Asked by At

I'm implementing a smart pointer class, and having a couple of confusions. Would really appreciate if people could help me clarify.

1: I thought smart pointer class should have "new" in constructor, "delete" in desctructor. But I can't seem to find a place to put in "new"... So the user will be responsible to create new, while the smart pointer class helps to clean it up?

2: In designing copy assignment operator, a popular approach was to copy-n-swap in order to be thread-safe. However, copy-n-swap requires the object is passed in by value (not by reference). Can it still be used in designing a smart pointer? My concern was that this is a pointer class, hence may not be able to pass in by value. But I'm not very sure about this.

3: If I'm requested to code a smart pointer at interview, do I have to provide some type of reference count? Thought reference count is specific to the shared_ptr...

4: This must be a dumb question, but better ask then keep doubts inside. for SmartPointer<T>& sp to access ptr, should it use sp->ptr or sp.ptr??

Appreciate your input.

template <class T>
class SmartPointer{
    T* ptr;
public:
    SmartPointer():ptr(NULL){}
    SmartPointer(const T& p):ptr(p){}
    SmartPointer(const SmartPointer<T>& sp):ptr(sp->ptr){}
    SmartPointer<T>& operator=(const SmartPointer<T>& sp);
    ~SmartPointer(){delete ptr;}
    T& operator*(){return *ptr;}
    T* operator->(){return ptr;}
};

SmartPointer& operator=(const SmartPointer& sp){
    T* pOrig = ptr;
    ptr = new T(sp.ptr);
    delete pOrig;
    return *this;
}
2

There are 2 answers

0
psliwa On

1: I would say the constructor is such place:

SmartPointer(T* p):ptr(p){}

then you gain the same syntax as in typical smart pointer - you create it by calling SmartPointer objPtr(new Obj()); . User can create his object however he wants and the smart pointer is responsible to clean it up after him (as it's the major issue smart pointers was designed to help with). Note, that in future you may want to add some kind of std::make_shared() method to avoid copying your wrapper while constructing and improve performance.


2: You have to think of the way your smart pointer will behave. Remember that when you try to copy a raw pointer you copy address only, not an object itself. So, if you decide to treat SmartPointer like a normal pointer equipped with garbage collector then you can freely implement copy-n-swap. On the other hand, you can delete operator=() like it is in std::unique_ptr or try to implement reference counting like in std::shared_ptr.


3: I'd say only if you are requested to do so explicitly. Normally by smart pointer one can understand some kind of wrapper that is able to control raw pointer's lifetime by itself. Although, I'm sure if they ask "please write a simple smart pointer's implementation" and you ask "do you want it to handle shared ownership as well?", you will gain a point :)


4: sp.ptr of course :) SmartPointer<T>& sp is a reference so in order to access its member you have to use a dot . operator.

To use sp->ptr you would have to have had SmartPointer<T>* sp parameter.

0
AudioBubble On

So the user will be responsible to create new, while the smart pointer class helps to clean it up?

Yes, typically this is what is done. Example:

std::unique_ptr<Foo> smart_ptr(new Foo);

One of the reasons the construction (and allocation) is left to the client is because T could have all kinds of parameterized constructors. It would be tricky, to say the least, and probably involve variadic constructor templates with some really fancy template magic if you wanted to allow a smart pointer to construct T on its own with all possibilities. It's simpler just to let the client pass in the pointer on constructing the smart pointer. As long as they do that right away on construction, it's safe, since if operator new throws before we get to the smart pointer construction, nothing will be allocated/constructed, and so there will be nothing to clean up except what's already on the call stack (which should conform to RAII for exception safety).

If you want to make it robust for API boundaries, then you usually want to capture a "deleter" which will call 'operator delete' at the same site in which the code was generated (important when working across module boundaries where trying to free memory in module B for memory allocated in module A would yield undefined behavior).

However, copy-n-swap requires the object is passed in by value (not by reference). Can it still be used in designing a smart pointer? My concern was that this is a pointer class, hence may not be able to pass in by value. But I'm not very sure about this.

For this kind of smart pointer you are making which implements no reference counting, typically the best design is to disallow copying (both assignment and copy ctor, though move ctors are fine).

Otherwise you're going back to the archaic kind of practices of transferring ownership (as is the case with the ancient std::auto_ptr) or trying to deep copy the pointee.

Transferring ownership can be especially prone to human error, since it treats the source it's copying from as mutable (which is completely unusual and trippy behavior). If you do that though, you can use atomic CAS to swap pointers, but you need to make the copy constructor and assignment operator accept things by reference, not const reference or by value, since it's going to treat the source as mutable (either that or make the private ptr member you have mutable and use const reference).

Deep copying the pointee is a kind of interesting idea, but one problem with that is that someone may try to copy your smart pointer at a site where T is not a complete type (not defined, only declared). It's similar to the problem with destructors, so if you want to make a deep copying smart pointer like this, a robust solution would be to capture the copy constructor of T (ex: store a function pointer to a function you generate when your smart pointer is constructed to clone/copy construct new elements of T).

Also slight fix for your existing deep-copying code:

SmartPointer& operator=(const SmartPointer& sp){
    T* pOrig = ptr;
    ptr = new T(*sp.ptr); <-- need to dereference here
    delete pOrig;
    return *this;
}

3: This must be a dumb question, but better ask then keep doubts inside. for SmartPointer& sp to access ptr, should it use sp->ptr or sp.ptr??

Let the compiler fix your doubts. Given a reference, SmartPointer& sp, sp->ptr would be a compiler error in that case unless T has a member called ptr you can access, which is probably not what you want anyway. sp->ptr would call your overloaded operator->, not access the actual private member of the smart pointer. operator-> is only defined normally for pointers, so trying to use it on a reference or const reference would invoke your overloaded, user-defined operators.