Topic: Error in tr1::weak_ptr design.
Author: "Lance Diduck" <lancediduck@nyc.rr.com>
Date: Sun, 25 Jun 2006 20:17:13 CST Raw View
Peter Dimov wrote:
> It is an explicit design decision that a weak_ptr only exposes two
> states, empty/expired and valid. I'm aware of the claims that it is
> sometimes desirable to distinguish between empty and expired, but at
> the moment, I haven't yet been convinced that the original design is
> faulty.
I think most of the confusion is in the name itself "weak pointer"
which sounds like it should point to something, Or can be dereferenced
into some memory. "weak references" are an artifact of reference
counted resource management, nothing more. See "Garbage Collection" by
Jones for the non C++ explaination of the method.
But do I really care about the difference of a weak pointer pointing to
an expired strong reference, to one that has never had it guts
initialized in the first place? I have implementations of weak_ptr that
does not have a counter. (Imagine giving intrusive_ptr weak references.
It is possible, using 4 pointers (5 if you care about operator< being
invariant before and after expiry), and leaving shared_ptr at 1
pointer. This has the advantage of being allocator free, and pushing
the space/time overhead to the rarely used, but required, weak_ptr.
(But yes it is intrusive)
However, having to worry about the difference between a weak_ptr that
was default constructed, vs one that has a counter but is expired,
implies an implementation detail that there IS a counter. None is
required.
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: nagle@animats.com (John Nagle)
Date: Mon, 26 Jun 2006 14:28:14 GMT Raw View
Some related weak pointer questions:
1. I think I'm looking at an old version of TR1. Was weak_ptr::use_count
deleted from TR1?
2. What's the rationale for not providing "operator->"? One
should be able to dereference a weak pointer directly, with
an exception if the weak pointer has expired.
3. Related issue: did overloading of "operator." go into C++0x?
If so, there should be "shared references" and "weak references".
4. Is "lock" thread-safe? That is, are you guaranteed that if you
do a "lock", and you get a valid shared_ptr, the object wasn't
deleted during the "lock"? If shared_ptr is intended to be
thread-safe, so should be "lock".
John Nagle
Animats
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: "Peter Dimov" <pdimov@gmail.com>
Date: Mon, 26 Jun 2006 16:29:44 CST Raw View
John Nagle wrote:
> Some related weak pointer questions:
>
> 1. I think I'm looking at an old version of TR1. Was weak_ptr::use_count
> deleted from TR1?
I believe that weak_ptr::use_count was/is present in all versions of
TR1
> 4. Is "lock" thread-safe? That is, are you guaranteed that if you
> do a "lock", and you get a valid shared_ptr, the object wasn't
> deleted during the "lock"? If shared_ptr is intended to be
> thread-safe, so should be "lock".
Yes, lock is thread-safe.
> 2. What's the rationale for not providing "operator->"? One
> should be able to dereference a weak pointer directly, with
> an exception if the weak pointer has expired.
T* operator-> is not present for the same reason T* get() isn't
present; there is no way to guarantee that the returned pointer is
valid.
In principle, one can solve that by providing
shared_ptr<T> operator-> () const;
but it only encourages subtle mistakes:
wp->f();
wp->g();
The programmer probably expects for f() and g() to be either both
executed or not; an exception from the second operator-> would be
sufficiently rare as to not show up on most tests. This is effectively
a race condition similar to
lock
f()
unlock
lock
g()
unlock
whereas the correct sequence is
lock
f()
g()
unlock
or, in weak_ptr speak,
if( shared_ptr<T> pt = wp.lock() )
{
pt->f();
pt->g();
}
else
{
// handle expiration
}
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: John Nagle <nagle@animats.com>
Date: Tue, 27 Jun 2006 09:55:43 CST Raw View
Peter Dimov wrote:
> John Nagle wrote:
>>Some related weak pointer questions:
>>2. What's the rationale for not providing "operator->"? One
>>should be able to dereference a weak pointer directly, with
>>an exception if the weak pointer has expired.
>
>
> T* operator-> is not present for the same reason T* get() isn't
> present; there is no way to guarantee that the returned pointer is
> valid.
>
> In principle, one can solve that by providing
>
> shared_ptr<T> operator-> () const;
>
> but it only encourages subtle mistakes:
>
> wp->f();
> wp->g();
>
> The programmer probably expects for f() and g() to be either both
> executed or not; an exception from the second operator-> would be
> sufficiently rare as to not show up on most tests. This is effectively
> a race condition similar to
>
> lock
> f()
> unlock
>
> lock
> g()
> unlock
That seems an unnecessary protection. It's not a memory model
soundness issue. It's not even a race condition issue; if there's
a problem, you get a well-defined exception.
A terse way of using a weak pointer is valuable.
If you have to write
if( shared_ptr<T> pt = wp.lock() )
{
pt->f();
}
else
{
// handle expiration
}
instead of
wp->f();
that will discourage the use of weak pointers. Programmers will be
tempted to use raw pointers for backlinks.
I'd suggest adding
shared_ptr<T> operator-> () const;
to weak_ptr. It's sound, it's a significant convenience, and it's
not easily added correctly by users.
John Nagle
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: Carl Barron <cbarron413@adelphia.net>
Date: Tue, 27 Jun 2006 22:55:26 CST Raw View
In article <k90og.109308$H71.24023@newssvr13.news.prodigy.com>, John
Nagle <nagle@animats.com> wrote:
> I'd suggest adding
>
> shared_ptr<T> operator-> () const;
>
> to weak_ptr. It's sound, it's a significant convenience, and it's
> not easily added correctly by users.
>
> John Nagle
>
there already is
shared_ptr<T>(wp)->f();
This creates a shared_ptr<T> from wp , throwing bad_weak_ptr if wp
has expired then uses shared_ptr<T>::operator ->() to access f.
Disposing of the shared pointer on return of f.
I'd guess that this could be a convenience method [as could operator *]
in fact it probably requres a shared ptr to be created via
shared_ptr(weak_ptr const &) and is simply
T & operator *() {return shared_ptr<T>(*this).operator *();}
T * operator ->() {return shared_ptr<T>(*this).operator->();}
off the cuff
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: "Joe Gottman" <jgottman@carolina.rr.com>
Date: Wed, 28 Jun 2006 01:19:37 CST Raw View
"Carl Barron" <cbarron413@adelphia.net> wrote in message
news:270620062249352677%cbarron413@adelphia.net...
> In article <k90og.109308$H71.24023@newssvr13.news.prodigy.com>, John
> Nagle <nagle@animats.com> wrote:
>
>> I'd suggest adding
>>
>> shared_ptr<T> operator-> () const;
>>
>> to weak_ptr. It's sound, it's a significant convenience, and it's
>> not easily added correctly by users.
>>
>> John Nagle
>>
> there already is
> shared_ptr<T>(wp)->f();
>
> This creates a shared_ptr<T> from wp , throwing bad_weak_ptr if wp
> has expired then uses shared_ptr<T>::operator ->() to access f.
> Disposing of the shared pointer on return of f.
>
> I'd guess that this could be a convenience method [as could operator *]
> in fact it probably requres a shared ptr to be created via
> shared_ptr(weak_ptr const &) and is simply
>
> T & operator *() {return shared_ptr<T>(*this).operator *();}
> T * operator ->() {return shared_ptr<T>(*this).operator->();}
>
> off the cuff
This could result in some nasty time-bombs in the code if users wrote
code like
weak_ptr<string> p = foo();
const string &s = *p;
The string reference s could be invalidated at any time, even in a
single-threaded program. This is even more unsafe than code using vector
iterators, because the circumstances under which a vector's iterators are
well-known and can be protected against (for instance by using reserve() at
the right time.) With shared_ptr's and weak_ptr's you really don't know who
else is sharing the code, so it is very difficult to protect yourself from
unexpected changes and (in the weak_ptr case) deletions.
Joe Gottman
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: "Peter Dimov" <pdimov@gmail.com>
Date: Wed, 28 Jun 2006 08:22:14 CST Raw View
John Nagle wrote:
> A terse way of using a weak pointer is valuable.
> If you have to write
>
> if( shared_ptr<T> pt = wp.lock() )
> {
> pt->f();
> }
> else
> {
> // handle expiration
> }
>
> instead of
>
> wp->f();
>
> that will discourage the use of weak pointers.
You have to write the longer version if you need to do something when
the pointer expires, such as making it point to somewhere else.
You can create a shared_ptr from wp
shared_ptr<T> pt( wp );
if you want an exception on expiration.
The problem with
wp->f();
again is that in this code:
wp->f();
wp->g();
wp->h();
a temporary shared_ptr is (implicitly and invisibly) created for each
line. Without an operator->, the programmer has to write
shared_ptr<T>(wp)->f();
shared_ptr<T>(wp)->g();
shared_ptr<T>(wp)->h();
and the inefficiency becomes obvious. (If the rest of the program
doesn't expect the second or third lines to fail, it's also a
correctness issue.)
shared_ptr<T> pt( wp );
pt->f();
pt->g();
pt->h();
> Programmers will be tempted to use raw pointers for backlinks.
There is nothing wrong with using a raw pointer for a child-parent
backlink, if the code is correct. :-)
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: alanwj@no.spam.stanford.edu (Alan Johnson)
Date: Thu, 1 Jun 2006 01:51:43 GMT Raw View
Greg Herlihy wrote:
> Alan Johnson wrote:
>> Consider the following function:
>>
>> void f(T * p)
>> {
>> if (p)
>> {
>> // Do something interesting with p.
>> }
>>
>> // Do more interesting stuff.
>> }
>>
>> It is not uncommon for a function to allow a pointer argument to be
>> NULL, and adjust its behavior accordingly. One would like equivalent
>> behavior from weak_ptr. For example:
>>
>> void f(weak_ptr<T> wp)
>> {
>> shared_ptr<T> sp(wp) ;
>> if (sp)
>> {
>> // Do something interesting with sp.
>> }
>>
>> // Do more interesting stuff.
>> }
>>
>> However, the code above is not correct. If an empty weak_ptr
>> (equivalent to a NULL pointer) is used to construct a shared_ptr, then
>> bad_weak_ptr is thrown. Thus, there is no distinguishing between an
>> empty weak_ptr and an expired weak_ptr.
>>
>> shared_ptr's constructor throws "bad_weak_ptr when r.expired()" (r being
>> the weak_ptr used to construct it), and this is as it should be.
>> However, weak_ptr's expired member returns "use_count() == 0", which is
>> an error, and the point of this post. A weak_ptr that is empty cannot,
>> by definition, expire. expired() should always return false for an
>> empty weak_ptr.
>
> A "false" result returned by weak_ptr::expire() can mean only that the
> weak_ptr has a valid pointer (and one that remains allocated). Allowing
> for any other intepretation of a false result would render the result
> meaningless. In particular, the proposed change (to have expire()
> return false when the weak_ptr was initialized without a pointer) would
> mean that a program obtaining a false result from expire() would know
> that one of two possibilites had to be true about the weak_ptr: that
> either it is NULL or it is not NULL. But those are the only two
> possible states that exist for a weak_ptr anyway, so a program would
> have learned nothing from having called expire().
>
I disagree with the conclusion that a weak_ptr has only two states.
There are three states: a) a NULL pointer (empty), b) an expired
pointer (non-NULL), c) a pointer to a valid object.
Calling expired currently allows the client to distinguish between
states (a or b) and c. I've proposed changing the meaning to
distinguishing between (a or c) and b. With this proposal, one could
then further distinguish between a and c by constructing a shared_ptr
(or by calling lock).
In its current form there is no way to distinguish between a and b.
(There is, but it is very contrived).
> A "true" result from calling expire() fares little better with this
> proposed change. Only now the interpretation of the result is not
> useless - but ambiguous. Because what exactly is an "empty" weak_ptr?
> Is a weak_ptr instantiated from an expired weak_ptr itself empty or
> expired? And does an empty weak_ptr assigned an expired weak_ptr remain
> empty or does it too become expired? There is no way that a weak_ptr
> could deal with these questions in a universal fashion since it is the
> program's own logic that would have to decide the answer to these
> questions (by identifying the lifespans of the pointers in question) -
> if indeed the program has any interest in asking these kinds of
> questions in the first place. In other words, maintaining a distinction
> between empty and expired weak_ptr's is not anything that can be
> implemented within the scope of the std::tr1::weak_ptr class template.
I had a rather long response to this typed up before I realized your
point. It would not be valid for a weak_ptr constructed from an expired
weak_ptr to simply copy the pointer, as that would invoke undefined
behavior. Looking at TR1, it states as the effects: "If r is empty,
constructs an empty weak_ptr object; otherwise, constructs a weak_ptr
object that shares ownership with r and stores a copy of the pointer
stored in r."
I find the wording somewhat confusing, as I don't think of a weak_ptr
"owning" anything to begin with. It seems to lack a definition for an
"empty" weak_ptr other than what you get as a result of default
construction. It also seems to ignore what happens when you try to
construct a weak_ptr from an expired weak_ptr. So I would argue that
TR1 doesn't sufficiently describe what should happen.
Looking a Boost's implementation, constructing a weak_ptr from an
expired weak_ptr will result in an empty weak_ptr, which is, I believe,
an incorrect thing to do. What should happen (by my definition of
should, not TR1's), is that you should somehow wind up with another
expired weak_ptr, though any implementation that could do that without
invoking undefined behavior would have to add at least another bit to
weak_ptr's size (to act as an "expired" boolean) to avoid the undefined
behavior of copying an invalid pointer.
> I think the actual problem demonstrated in the example is that the
> called routine, f(), constructs a shared_ptr from the weak_ptr instead
> of obtaining the shared_ptr by calling the weak_ptr's lock() method.
> The former technique necessarily assumes that the weak_ptr is not NULL
> - an assumption that flatly contradicts the stated premise that the
> weak_ptr may be NULL. Therefore, calling lock() on the weak_ptr is the
> appropriate method to obtain a shared_ptr in this example and in any
> case in which the program allows the possibility that the weak_ptr
> being accessed may be NULL.
Calling lock() isn't sufficient to distinguish Case a and Case b. It
will return NULL in both cases. When what is desired is for
bad_weak_ptr to be thrown in the case of an expired weak_ptr, but an
empty shared_ptr to be constructed in the case of an empty weak_ptr.
Currently, the only way to distinguish between Case a and Case b is
something like:
weak_ptr<T> empty ;
if (!(wp < empty) && !(empty < wp))
{
// wp is empty
}
But even so, with Boost's implementation, one cannot even use this on
copies of an expired weak_ptr, since copies form an empty weak_ptr.
--
Alan Johnson
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: "Peter Dimov" <pdimov@gmail.com>
Date: Thu, 1 Jun 2006 09:34:47 CST Raw View
Alan Johnson wrote:
> I disagree with the conclusion that a weak_ptr has only two states.
> There are three states: a) a NULL pointer (empty), b) an expired
> pointer (non-NULL), c) a pointer to a valid object.
>
> Calling expired currently allows the client to distinguish between
> states (a or b) and c. I've proposed changing the meaning to
> distinguishing between (a or c) and b. With this proposal, one could
> then further distinguish between a and c by constructing a shared_ptr
> (or by calling lock).
>
> In its current form there is no way to distinguish between a and b.
It is an explicit design decision that a weak_ptr only exposes two
states, empty/expired and valid. I'm aware of the claims that it is
sometimes desirable to distinguish between empty and expired, but at
the moment, I haven't yet been convinced that the original design is
faulty.
In my opinion, both designs are legitimate.
> (There is, but it is very contrived).
Indeed. In the original weak_ptr proposal, this way didn't even work.
> I had a rather long response to this typed up before I realized your
> point. It would not be valid for a weak_ptr constructed from an expired
> weak_ptr to simply copy the pointer, as that would invoke undefined
> behavior. Looking at TR1, it states as the effects: "If r is empty,
> constructs an empty weak_ptr object; otherwise, constructs a weak_ptr
> object that shares ownership with r and stores a copy of the pointer
> stored in r."
>
> I find the wording somewhat confusing, as I don't think of a weak_ptr
> "owning" anything to begin with. It seems to lack a definition for an
> "empty" weak_ptr other than what you get as a result of default
> construction.
This _is_ the definition of an empty weak_ptr.
> It also seems to ignore what happens when you try to
> construct a weak_ptr from an expired weak_ptr.
Under the original design, where there was no way to distinguish
between empty and expired, there was no need to clarify whether the
result of copying an expired weak_ptr is an empty or an expired
weak_ptr.
Under a strict reading of the current wording, the above clearly
specifies that the result of copying an expired weak_ptr is another
expired weak_ptr that shares ownership with the original.
> So I would argue that TR1 doesn't sufficiently describe what should happen.
You are welcome to submit a defect report and explain what you think
TR1 should say.
> Looking a Boost's implementation, constructing a weak_ptr from an
> expired weak_ptr will result in an empty weak_ptr, which is, I believe,
> an incorrect thing to do.
Try it. The result is another expired weak_ptr that shares ownership
with the original.
> What should happen (by my definition of
> should, not TR1's), is that you should somehow wind up with another
> expired weak_ptr, though any implementation that could do that without
> invoking undefined behavior would have to add at least another bit to
> weak_ptr's size (to act as an "expired" boolean) to avoid the undefined
> behavior of copying an invalid pointer.
Indeed. The original rationale for conflating empty and expired was
that if the user needs a pair< weak_ptr, bool >, then it is easy to
obtain one. Furthermore, in most cases, the additional bool is implicit
in the design, as in map< int, weak_ptr >, for example.
It is also convenient to .reset() a weak_ptr and have the rest of the
code treat it as expired, without needing to actually destroy the
pointee.
However, as the Boost implementation already has enough information to
answer .is_empty(), and preserves the empty/expired state when copying,
I (personally) would probably not object to a formal request to add
bool is_empty() const to weak_ptr.
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: alanwj@no.spam.stanford.edu (Alan Johnson)
Date: Tue, 30 May 2006 22:00:32 GMT Raw View
Consider the following function:
void f(T * p)
{
if (p)
{
// Do something interesting with p.
}
// Do more interesting stuff.
}
It is not uncommon for a function to allow a pointer argument to be
NULL, and adjust its behavior accordingly. One would like equivalent
behavior from weak_ptr. For example:
void f(weak_ptr<T> wp)
{
shared_ptr<T> sp(wp) ;
if (sp)
{
// Do something interesting with sp.
}
// Do more interesting stuff.
}
However, the code above is not correct. If an empty weak_ptr
(equivalent to a NULL pointer) is used to construct a shared_ptr, then
bad_weak_ptr is thrown. Thus, there is no distinguishing between an
empty weak_ptr and an expired weak_ptr.
shared_ptr's constructor throws "bad_weak_ptr when r.expired()" (r being
the weak_ptr used to construct it), and this is as it should be.
However, weak_ptr's expired member returns "use_count() == 0", which is
an error, and the point of this post. A weak_ptr that is empty cannot,
by definition, expire. expired() should always return false for an
empty weak_ptr.
--
Alan Johnson
---
[ 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.comeaucomputing.com/csc/faq.html ]
Author: "Greg Herlihy" <greghe@pacbell.net>
Date: Wed, 31 May 2006 02:02:05 CST Raw View
Alan Johnson wrote:
> Consider the following function:
>
> void f(T * p)
> {
> if (p)
> {
> // Do something interesting with p.
> }
>
> // Do more interesting stuff.
> }
>
> It is not uncommon for a function to allow a pointer argument to be
> NULL, and adjust its behavior accordingly. One would like equivalent
> behavior from weak_ptr. For example:
>
> void f(weak_ptr<T> wp)
> {
> shared_ptr<T> sp(wp) ;
> if (sp)
> {
> // Do something interesting with sp.
> }
>
> // Do more interesting stuff.
> }
>
> However, the code above is not correct. If an empty weak_ptr
> (equivalent to a NULL pointer) is used to construct a shared_ptr, then
> bad_weak_ptr is thrown. Thus, there is no distinguishing between an
> empty weak_ptr and an expired weak_ptr.
>
> shared_ptr's constructor throws "bad_weak_ptr when r.expired()" (r being
> the weak_ptr used to construct it), and this is as it should be.
> However, weak_ptr's expired member returns "use_count() == 0", which is
> an error, and the point of this post. A weak_ptr that is empty cannot,
> by definition, expire. expired() should always return false for an
> empty weak_ptr.
A "false" result returned by weak_ptr::expire() can mean only that the
weak_ptr has a valid pointer (and one that remains allocated). Allowing
for any other intepretation of a false result would render the result
meaningless. In particular, the proposed change (to have expire()
return false when the weak_ptr was initialized without a pointer) would
mean that a program obtaining a false result from expire() would know
that one of two possibilites had to be true about the weak_ptr: that
either it is NULL or it is not NULL. But those are the only two
possible states that exist for a weak_ptr anyway, so a program would
have learned nothing from having called expire().
A "true" result from calling expire() fares little better with this
proposed change. Only now the interpretation of the result is not
useless - but ambiguous. Because what exactly is an "empty" weak_ptr?
Is a weak_ptr instantiated from an expired weak_ptr itself empty or
expired? And does an empty weak_ptr assigned an expired weak_ptr remain
empty or does it too become expired? There is no way that a weak_ptr
could deal with these questions in a universal fashion since it is the
program's own logic that would have to decide the answer to these
questions (by identifying the lifespans of the pointers in question) -
if indeed the program has any interest in asking these kinds of
questions in the first place. In other words, maintaining a distinction
between empty and expired weak_ptr's is not anything that can be
implemented within the scope of the std::tr1::weak_ptr class template.
I think the actual problem demonstrated in the example is that the
called routine, f(), constructs a shared_ptr from the weak_ptr instead
of obtaining the shared_ptr by calling the weak_ptr's lock() method.
The former technique necessarily assumes that the weak_ptr is not NULL
- an assumption that flatly contradicts the stated premise that the
weak_ptr may be NULL. Therefore, calling lock() on the weak_ptr is the
appropriate method to obtain a shared_ptr in this example and in any
case in which the program allows the possibility that the weak_ptr
being accessed may be NULL.
Greg
---
[ 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.comeaucomputing.com/csc/faq.html ]