Topic: Smart pointers revisited: 3 suggestions
Author: tylo@start.no (T. Lovset)
Date: Mon, 26 Jan 2004 22:38:18 +0000 (UTC) Raw View
> To transform a naked pointer belonging to a class into a shared_ptr,
> one must first explore whether the class has a has a "use"- or a
> "has"-relation to the pointee class. Only if it is a *has*-relation, it
> should be converted to a smart ptr, otherwise it can be left alone or
> converted to a weak pointer.
The above is a little unprecise, so I will comment a little more.
Sometimes several classes can share ownership of another class (this
is where shared_ptrs are indispensable). Then there is both a use- and
a has- relation to that class. Thus, the constructor, or another
method, receives a pointer so that it can be used (and possibly
transfer ownership). This parameter must be changed to a smart
pointer, like
class X {
public:
X(... const shared_ptr<Y>& p) : ... m_p(p)
shared_ptr<Y> m_p;
};
My point is the same: if we forget to modify the interface where the
pointer is received, it won't be catched by the compiler, and it is
fatal.
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.jamesd.demon.co.uk/csc/faq.html ]
Author: tylo@start.no (T. Lovset)
Date: Mon, 26 Jan 2004 22:38:37 +0000 (UTC) Raw View
Sorry for all the post, but I forgot to mention a last feature with my
proposal: You can get rid of reset(). That function makes life harder
when going from raw pointers to use smart pointers (and vice versa):
To summarize:
The current smart pointers:
shared_ptr<T> q, p( new MyObject("me", 1) );
q = p;
p.reset( new MyObject("you", 2) ); // deref, assign a new raw ptr.
cout << p.get() << std::endl; // must use get()
p.reset(); // deref, then assign NULL
My proposal:
shared_ptr<T> q = NULL, p = new1 (MyObject("me", 1)); // =NULL is opt.
q = p;
p = new1 (MyObject("you", 2));
cout << p << std::endl; // auto converts to an anonymous ptr
p = NULL;
In my opinion, this is a more natural syntax, and makes it simpler for
newcomers to use. The close resemblance to raw pointer syntax also
makes it faster to change from using raw to smart pointers. Finally,
it is less error-phone, in that it is less likely to make wrong/
illegal initializations of smart pointers.
T. Lovset.
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.jamesd.demon.co.uk/csc/faq.html ]
Author: tylo@start.no (T. Lovset)
Date: Tue, 27 Jan 2004 04:17:06 +0000 (UTC) Raw View
A simple demo implementation, and some test code:
demo::shared_ptr<int> fine = new1 (int(10)); // yes
demo::shared_ptr<int> fail1( new int(20) ); // NO
demo::shared_ptr<int> fail2 = new int(30); // NO
std::cout << fine << std::endl; // yes - print address
int* fail3 = fine; // NO
int* fine2 = fine.get(); // yes
fine = NULL; // yes
----------
#ifndef demo_shared_ptr_h
#define demo_shared_ptr_h
// A special global new operator
#define new1(C) demo::make_new1_handle(new C)
namespace demo {
// Object holder class.
template <typename T> class new1_handle
{
public:
explicit new1_handle(T* p) : px(p) {}
T* release() const { return px; }
private:
new1_handle(const new1_handle&);
new1_handle& operator=(const new1_handle&);
T* px;
};
// Function to instanciate a new1_handler object
template <typename T> inline new1_handle<T> make_new1_handle(T* p)
{
return new1_handle<T>(p);
}
template <typename T> class shared_ptr
{
private:
class null_tag {};
class anon_tag { ~anon_tag(); };
public:
typedef T element_type;
typedef T value_type;
typedef T * pointer;
typedef long shared_count;
shared_ptr(null_tag* = 0)
: px(0), pn(0) {}
template <typename U>
shared_ptr(const new1_handle<U>& m)
: px(m.release()), pn(new shared_count(1)) {}
// must be defined before non-templated version for VC++
template <typename U >
shared_ptr(const shared_ptr<U>& x)
: px(x.get()), pn(x.__pn()) { ref(); }
shared_ptr(const shared_ptr& x)
: px(x.px), pn(x.pn) { ref(); }
~shared_ptr() {unref(); }
shared_ptr& operator=(null_tag*) {
unref();
px = 0;
pn = 0;
return *this;
}
template <typename U>
shared_ptr& operator=(const new1_handle<U>& m) {
unref();
px = m.release();
pn = new shared_count(1);
return *this;
}
// must be defined before non-templated version for VC6
template <typename U>
shared_ptr& operator=(const shared_ptr<U>& x) {
if (x.get() != px) {
unref();
px = x.get();
pn = x.__pn();
ref();
}
return *this;
}
shared_ptr& operator=(const shared_ptr& x) {
if (x.px != px) {
unref();
px = x.px;
pn = x.pn;
ref();
}
return *this;
}
T& operator*() const { return *px; }
T* operator->() const { return px; }
T* get() const { return px; }
operator const anon_tag*() const {
return reinterpret_cast<const anon_tag*>(px);
}
long use_count() const { return px ? long(*pn) : 0; }
bool unique() const { return use_count() == 1; }
void swap(shared_ptr& x) {
std::swap(px, x.px);
std::swap(pn, x.pn);
}
private:
void ref() {
if (px) ++*pn;
}
void unref() {
if (px && --*pn == 0) {
typedef char X[sizeof(T)]; // checked delete.
delete px;
delete pn;
}
}
T* px;
shared_count* pn;
public: // really private:
shared_ptr(T* p, shared_count* r) : px(p), pn(r) { ref(); }
shared_count* __pn() const { return pn; }
};
template <typename T> inline
bool operator<(const shared_ptr<T>& x, const shared_ptr<T>& y) {
return x.get() < y.get();
}
template <typename T, typename U> inline
bool operator==(const shared_ptr<T>& x, const shared_ptr<U>& y) {
return x.get() == y.get();
}
template <typename T, typename U> inline
bool operator==(const shared_ptr<T>& x, const U* y) {
return x.get() == y;
}
template <typename T, typename U> inline
bool operator!=(const shared_ptr<T>& x, const shared_ptr<U>& y) {
return x.get() != y.get();
}
template <typename T, typename U> inline
bool operator!=(const shared_ptr<T>& x, const U* y) {
return x.get() != y;
}
} // demo::
template <typename T> inline
void swap(demo::shared_ptr<T>& x, demo::shared_ptr<T>& y)
{
x.swap(y);
}
template <typename T, typename U> inline
demo::shared_ptr<T> static_pointer_cast(const demo::shared_ptr<U>& x)
{
return demo::shared_ptr<T>(static_cast<T*>(x.get()), x.__pn());
}
template <typename T, typename U> inline
demo::shared_ptr<T> dynamic_pointer_cast(const demo::shared_ptr<U>& x)
{
T* p = dynamic_cast<T*>(x.get());
return p ? demo::shared_ptr<T>(p, x.__pn()) : demo::shared_ptr<T>();
}
#endif
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.jamesd.demon.co.uk/csc/faq.html ]
Author: tylo@start.no (T. Lovset)
Date: Sun, 25 Jan 2004 20:04:38 +0000 (UTC) Raw View
First, thanks for all answers on my memmove reserve() suggestion. I
realize that it is unsafe, though we have to pay a rather high
overhead price for those rare cases where the vector elements are
location-dependent. The new proposal for the move semantics could
change all that.
Now, I have a few suggestion on improving smart pointers. I know that
smart pointers have been discussed in depth in these forums, but at
least some of these suggestions have not been proposed before.
I talk mostly about shared_ptr, but the following suggestions also
applies to other smart pointers, like auto_ptr.
1. THE DANGEROUS SMART POINTER CONSTRUCTOR
In my effort to transform projects from using naked pointer to
shared_ptr where possible, I found a specific pitfall, which can be
eliminated by modifying the smart pointer interface. In addition, it
makes the interface and usage of smart pointer more intuitive.
A shared_ptr is initialized by a naked ptr. That is with an explicit
constructor, like,
shared_ptr<X> ptr(new X(..)).
It is required that the pointer is directly created by new.
To transform a naked pointer belonging to a class into a shared_ptr,
one must first explore whether the class has a has a "use"- or a
"has"-relation to the pointee class. Only if it is a use-relation, it
should be converted to a smart ptr, otherwise it can be left alone or
converted to a weak pointer.
The trouble is: When converting a large project, the chance of making
a false decision is present:
// Obviously a use-relation
class X {
X(..., Y* p) : ... m_p(p) {}
Y* m_p;
...
};
// Wrongly converted to shared_ptr
class X {
X(..., Y* p) : ... m_p(p) {}
shared_ptr<Y> m_p;
...
};
The latter wrong convertion is not catched by the compiler, and that
has fatal consequences. The problem is that the shared_ptr "explicit"
constructor does not help to avoid a wrong initialization of the
pointer here.
How can this be catched by the compiler? One way is to add a parameter
to the shared_ptr constructor, like:
shared_ptr<Y> p(new Y(), dummy);
That makes the already awkward constructor even worse. My suggestion
is the following:
template <typename T>
class new1_handle {
public:
explicit new1_handle(T* p) : p__(p) {}
T* release() const { return p__; }
private:
new1_handle(const new1_handle&);
new1_handle& operator=(const new1_handle&);
/*volatile*/ T* p__;
};
template <typename T>
inline new1_handle<T> make_new1_handle(T* p) {
return new1_handle<T>(p);
}
So, instead of
template <typename Y>
explicit shared_ptr(Y * p): px(p), ...
I suggest:
template <typename Y>
shared_ptr(const new1_handle<Y>& h): px(h.release()), ...
Now, the above wrongly smart pointer initialization will be catched by
the compiler. But also other wrong usages of smart pointer
initializations will be exposed. Note that new1_handle::reset() could
also set p__=0, to make it more safe (requires volatile p__), and also
a destrutor could check that p__==0; (otherwise mem-leak).
Another benefit is that the constructor no longer need to be
explicit, and it becomes much more intuitive to use than the current
implementation:
// Macros should be avoided, but this time it act as
// a global operator, like operator new:
#define boost_new1(C) boost::make_new1_handle(new C)
shared_ptr<X> p = boost_new1 (X());
2. NULL-SETTING OF SMART POINTERS
I believe that this has been discussed before, but it is related to
the above. Again, these need not be "explicit":
class shared_ptr {
class null_tag;
public:
shared_ptr(null_tag* = NULL) : px(NULL), ..
shared_ptr& operator=(null_tag*); // reset to NULL
Which allows:
shared_ptr<X> p = NULL;
p = NULL;
3. THE EXPOSED GET METHOD
In principle, we should not need to use get() ever, if we use, both
shared and weak pointers all the way. In practice, get() is often
used. For the purpose of transforming into using smart pointers
instead of naked pointers, it is annoying to have to use get() where
it should not be necessary, e.g.
std::cout << p.get() ;
The following suggestion is probably somewhat more controversial:
class shared_ptr {
class null_tag;
class anon_tag { ~anon_tag(); };
public:
operator const anon_tag*() const {
return reinterpret_cast<const anon_tag*>(px);
}
AFAIK, this convert operator does not have any side effects. Note the
private destructor of anon_tag() which is there to disallow to use
delete on a smart pointer.
T. Lovset.
---
[ comp.std.c++ is moderated. To submit articles, try just posting with ]
[ your news-reader. If that fails, use mailto:std-c++@ncar.ucar.edu ]
[ --- Please see the FAQ before posting. --- ]
[ FAQ: http://www.jamesd.demon.co.uk/csc/faq.html ]