Effective C++ item 45: Use Member Function Templates to Accept "All Compatible Types"

Have you ever wonder how to implement a shared_ptr? This item is to make you think about how to implement the behavior that implicit pointer conversion provided by porlymorphism.

1
2
3
4
5
6
7
8
9
10
11
12
class Top {};
class Middle: public Top {};
class Bottom: public Middle {};

int main() {

Top *pt1 = new Middle;
Top *pt2 = new Bottom;
const Top *pct2 = pt1;

return 0;
}

You will be able to create derivated class pointer and assign it to base pointer, or assign a non-const pointer to a const pointer.

Now it’s your time to think about how to implement such behavior for SmartPtr so following operations are legal.

1
2
3
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;

The first attempt will be

1
2
3
4
5
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T *realPtr){}
};

And you will immediate realize that this won’t work. Because SmartPtr<Middle> and SmartPtr<Top> as completely different classes, no more closely related than, say, vector<float> and Widget. In order to make it work following this route, we have to implement conversion functions for every possible conversion we need to support. In principle, the number of constructors we need is unlimited. Because people will add derived class on top of current class and you don’t want to change SmartPtr‘s implementation in order to support new derived class.

Clearly we need a better way, which is member templates, or in this case generalized copy constructors.

1
2
3
4
5
6
7
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T *realPtr){}
template<typename U>
SmartPtr(const SmartPtr<U>& other){}
};

With this, we achieved what we want previously, to make following compile

1
2
3
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;

But wait a second. Follwing code will also compile which is complete nonsense

1
2
3
4
SmartPtr<Middle> pt1 = SmartPtr<Top>(new Middle);
SmartPtr<const Bottom> pct2 = SmartPtr<const Top>(new Bottom);
SmartPtr<Top> pt2 = pct2;
SmartPtr<Middle> pt1 = SmartPtr<double>(new double);

How do we apply restriction to our SmartPtr so only meaningful implicit type conversion is allowed? We need to rely on existing non-template verion of implicit type conversion. Here is how

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T *realPtr){}
template<typename U>
SmartPtr(const SmartPtr<U>& other)
: heldPtr(other.get()) {}

T* get() const { return heldPtr; }
private:
T *heldPtr;
};

Basically we accept all types in our copy constructor but rely on non-template implicit conversion to accept all compatible types. If we pass in an noncompatible type, compiler will give us error preventing us from doing so. Pretty neat right?

In fact, this is a very common technique to implement generalized copy constructor and generalized assignment operator.

One last thing to remember is, even you provide generalized copy constructor and generalized assignment operator, you still need to provide default copy constructor and default assignment operator. Otherwise compiler generated default will be used.

So end result looks like following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T *realPtr){}
SmartPtr(SmartPtr const& r){};
template<typename U>
SmartPtr(const SmartPtr<U>& other)
: heldPtr(other.get()) {}

SmartPtr& operator=(SmartPtr const& r){};
template<typename Y>
SmartPtr& operator=(SmartPtr<Y> const& r){};

T* get() const { return heldPtr; }
private:
T *heldPtr;
};

Reference:
“Effective C++” Third Edition by Scott Meyers.