Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Conversations:Getting to the Point


July 2002 C++ Experts Forum/Conversations


"Argh!"

It was my seventh cry of frustration in one hour. Wendy was obviously getting tired of them. "Look, pardner, pipe down over there, will ya?" her voice floated over the cubicle. I suppose that was better than her previous reaction.

I had been wrestling with auto_ptrs. Within the first few days of working for the Guru, I had learned the usefulness of auto_ptrs — and their not-so-obvious pitfalls [1]. I had been stung by auto_ptr's strange ownership semantics. The auto_ptr uses move operations that are disguised as copy operations. Imagine this scenario: you walk up to a photocopier, place your sheet in the feeder, and press the "Copy" button. A few moments later, the photocopier gives you a copy — then runs the original through a shredder. That pretty much sums up what the auto_ptr's copy and assignment operators do.

Anyway, here I was, a little wiser and more experienced, and I was still running into auto_ptr's various limitations. For one, an auto_ptr can't be used to hold pointers to arrays. I had toyed with hacking around that, by explicitly using array delete:

auto_ptr<T> autoArray(new T[10]);
// ...
delete [] autoArray.release();

but had dismissed that idea almost immediately. Even I could see the many dangers of such an "abomination" (as the Guru would call it). The whole, er, point of auto_ptr was to own and destroy the memory; doing it manually would completely defeat the purpose.

So, I decided to use a vector instead to own an array. It was a little more powerful than I needed, since I didn't need any dynamic resizing of the vector, but it served the purpose. What I really wanted was a smart pointer that would automatically handle the pointer and memory management, and that I could use to hold an array.

"Use the Boost, Luke," I heard the Guru's voice behind me.

"Isn't that 'Use the Force, Luke'?" I said over my shoulder. "And don't call me Luke."

"The Boost library can be a powerful ally," the Guru continued in her full act.

I wasn't in the mood today, though, so I interrupted. "Uh, can we dispense with the theatrics today? Bob's at our London office, and there are no interns to freak out."

She came around and I saw her smile and shrug. "Oh, I just felt like being silly," she sighed. "Have you checked out the Boost library's smart pointers?"

"Uh, well," I hesitated, "I haven't had a chance to look at the Boost library in detail. What's it got in the way of smart pointers?"

"There are five smart pointers in all," the Guru said as she sat down. "Two of them provide simple pointer ownership, and two provide shared ownership."

"Shared ownership? Oh, you mean like a reference-counted class?"

"Exactly. The smart pointers are in pairs, so you have one for pointers to single objects, and one for pointers to arrays." As she spoke, she began writing on the whiteboard: scoped_ptr, scoped_array, shared_ptr, and shared_array.

"That's only four; you said there were five?" I queried.

"The remaining one is called weak_ptr. It's a non-owning observer of a shared_ptr. I'll get to that one in a minute. The scoped_* pointers automatically destroy the object pointed to when they go out of scope. One suggested use is to implement the Pimpl Idiom — pointer to Implementation," she hastily added, forestalling my next pun.

"So... they're like auto_ptr, right?"

"Not quite. scoped_ptr and scoped_array are not copyable."

"Not copyable? So, if I had a pointer as a class member—" I said as I took up the marker and started scrawling on the whiteboard:

class T
{
 scoped_ptr<TImpl> p;
 ...

"—how would I implement copying and assignment?"

"The same as you would with an auto_ptr," the Guru replied, as she wrote:

T( const T& other )
: p(new TImpl(*other.p))
{
}

T &operator=( const T& other )
{
  scoped_ptr<TImpl> tmp(new TImpl(*other.p));
  p.swap(tmp);
}

"Ooookay," I drawled as I digested this. "So, when I'm using a scoped_ptr, I have to do all the work I'd have to do with an auto_ptr, right? So why not just use auto_ptr?"

"Because using an auto_ptr would compile, but do the wrong thing, for the automatically generated functions. Using scoped_ptr makes it much more difficult to accidentally overlook the copying semantics, because the compiler will refuse to compile the class if you use the compiler-generated versions. Also, a scoped_ptr cannot be used on an incomplete type." I gave the Guru my favorite deer-in-the-headlights look. She sighed. "Consider this," she said as she wrote on the whiteboard:

class Simple;

Simple * CreateSimple();

void f()
{
 auto_ptr<Simple> autoS(CreateSimple());
 scoped_ptr<Simple> scopedS(CreateSimple());
 //...
}

"Inside the function, Simple is an incomplete type. If its destructor is non-trivial, then destroying it through the auto_ptr or the scoped_ptr will result in undefined behavior. Some compilers will warn you about this when you instantiate the auto_ptr, but not all will. The scoped_ptr, on the other hand, does a little behind the scenes work to force a compiler error if it is instantiated with an incomplete type."

"Oh, right, of course," I jumped in, eager to show I understood. "So we should use scoped_ptr all the time, instead of auto_ptr, right?"

"Wrong, sorry," she said, sadly. "An auto_ptr still has its uses. Unlike auto_ptr, scoped_ptr has no release() member — it cannot relinquish ownership of the pointer. Because of this, and because a scoped_ptr cannot be copied, when a scoped_ptr goes out of scope, the pointer it manages will always be deleted. This makes scoped_ptr unsuitable for those occasions where you really want to transfer ownership, such as a factory.

"Using scoped_ptr telegraphs your intent to other programmers. It tells others, 'This pointer should not be copied outside the current scope.' An auto_ptr, on the other hand, grants permission to transfer ownership outside the scope of where the pointer was created, but it maintains control over the pointer until that transfer of ownership is complete. This is, of course, important in writing exception-safe code."

"Ah, okay, I got it," I mumbled. "What about the other smart pointers you mentioned?"

"scoped_array behaves the same as scoped_ptr, except that it manages an array of objects, not a single object. scoped_array provides slightly different access functions. It does not have operator * or operator->, but it does have operator []. I believe," she concluded, "that the smart pointer you need is a scoped_array pointer."

"Maybe," I replied, "but I think I'd like to understand more about these shared_* smart pointers before I decide."

"Very wise, my apprent... er, sorry, I slipped there." The Guru gave me a sheepish grin. "Yes, being familiar with your options before you make a decision is good engineering practice."

"The shared_* smart pointers," she pointed at shared_ptr and shared_array on the whiteboard, "are very powerful tools. They are reference-counting pointers, which can differentiate between owners and observers. For example, using the class T from above:"

void sharing()
{
 shared_ptr<T> firstShared (new T);
 shared_ptr<T> secondShared(firstShared);
 // ...
}

"Both shared_ptr objects refer to the same T object. The use count for the shared_ptr is now 2. Since the shared_ptr is reference counted, the object will not be destroyed until the last shared_ptr goes out of scope — when the use count falls to zero.

"The shared_* pointers are quite flexible, in that you can use them to hold pointers to incomplete types."

"I thought you said that was bad?"

"It is bad, if you are careless. The shared_* pointers can be instantiated using a pointer to an incomplete type, but only if you specify a function or function object that will be called when the owned object is destroyed. For example, say we modify our little function f above:"

void DestroySimple( Simple* );

void f()
{
 shared_ptr<Simple> sharedSFails( CreateSimple() );
 shared_ptr<Simple> sharedSSucceeds
 ( CreateSimple(), DestroySimple );
}

"The first shared_ptr fails, because Simple is an incomplete type. The second one succeeds, because you have explicitly told the shared_ptr how to destroy the pointer.

"Because the shared_ pointers are designed to be copied, they are well-suited for use in standard containers, including associative containers. And, on those rare occasions where casting is required, there are special cast operators defined, which create new shared_* pointers. For example, if we have the classes Base and Derived with the obvious public inheritance, and an unrelated class, then you could have:"

void g()
{
 shared_ptr<Base> sharedBase( new Derived );
 shared_ptr<Derived> sharedDer = 
 shared_dynamic_cast<Derived>( sharedBase );
 shared_ptr<Unrelated> sharedUnrelated = 
     shared_dynamic_cast<Unrelated>( sharedBase );
 try
 {
    shared_ptr<Unrelated> oops = 
     shared_polymorphic_cast<Unrelated>( sharedBase );
 }
 catch( bad_cast )
 {
    //...
 }
}

"There is also a shared_static_cast, for those rare occasions when it is needed," the Guru concluded.

I studied the function for a moment. "Okay, let me see if I can follow what's going on. The first shared_dynamic_cast performs a dynamic_cast on the smart pointer and returns a new smart pointer. What if the dynamic_cast fails — what happens to the use count?"

The Guru nodded sagely. "In that case, the original counter is not affected, and the new shared_ptr contains a null pointer. As you can probably infer from the try/catch clause, a shared_polymorphic_cast will also attempt to perform a dynamic_cast on the owned pointer. If the cast fails, it will throw a bad_cast exception."

"Wow," I wowed, "this really is quite the class. It looks really well thought out."

"Indeed," the Guru agreed. "There are a couple of things to be aware of, though. The reference counting is simple and cannot detect any cyclic references. Also, the shared_ptr does not implement any form of copy-on-write, so you must exercise caution if it is used to implement a Pimpl idiom."

I mulled it over and decided I liked it. "Hey," I added, remembering, "what about that weak_ptr you mentioned?"

"Ah, yes. A weak_ptr is used in conjunction with shared_ptrs. The weak_ptr is an observer, which does not affect the usage count of the shared object. The main purpose of weak_ptr is to allow the shared_ptr to participate in cyclic dependencies — A holds a reference to B, which in turn holds a reference to A. I like to think of weak_ptr as an Associate Member in a club — it has no voting privileges, but can attend the club meetings."

"Hmmm..." I thought this over a bit. "Since the weak_ptr doesn't affect the usage count, what happens if the club disbands — in other words, the last shared_ptr goes out of scope — while a weak_ptr is using a shared object?"

"In that case, the weak_ptr's managed pointer gets set to null. And speaking of null, none of the operators that involve dereferencing, such as operator *, operator ->, or operator [], check for a null pointer before dereferencing. So, just as you must check that a raw pointer is not null before dereferencing it, you must check that a smart pointer is not null. This restriction applies to all the smart pointers in the Boost library."

"Man, that's a lot of stuff to remember," I said, looking over my heavily-scribbled-upon whiteboard.

"Don't worry," the Guru smiled, as she stood up to leave. "I'll email you the URL for the Boost smart library and a small program that exercises and demonstrates the various pointers." She was, of course, true to her word, for within minutes I received an email from her. I rolled my eyes as soon as I read it:

    "My young apprentice, as promised here is a link to the blessed Boost writings: <www.boost.org/libs/smart_ptr/smart_ptr.htm> [2]. Read and meditate on the attached humble parable [3]."

Acknowledgements

Thanks to Peter Dimov and Bjorn Karlsson for providing valuable comments and updates.

Notes

[1] Jim Hyslop and Herb Sutter. "Conversations #1," C++ Report, April 2000.

[2] The most recent version of the library and documentation can also be obtained via anonymous CVS checkout from the Source Forge project. Detailed instructions can be found at: <http://sourceforge.net/projects/boost/>.

[3] The sample code can be downloaded from the CUJ website: hyslop.zip.

Jim Hyslop is a senior software designer at Leitch Technology International Inc. He can be reached at [email protected].

Herb Sutter (<www.gotw.ca>) is secretary of the ISO/ANSI C++ standards committee, author of the acclaimed books Exceptional C++ and More Exceptional C++, and one of the instructors of The C++ Seminar (<www.gotw.ca/cpp_seminar>). In addition to his independent writing and consulting, he is also C++ community liaison for Microsoft.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.