Feed Icon RSS 1.0 XML Feed available

Smart Pointer Casting Study

Date: 10-Jul-2009/19:44

Tags:

Characters: (none)

If you're using public inheritance in C++, the compiler will implicitly "upcast" from a Derived class pointer to a Base class pointer. So I thought a std::auto_ptr to a Derived class would have a similar implicit upcast. It does...but only for assignment and construction!
The example shows that if you try to pass an auto_ptr<Derived> to a function that takes an auto_ptr<Base>, it fails to convert due to an ambiguity. You must put an explicit static_cast<&nbsp;auto_ptr<Base>&nbsp;> at the calling site.
This "minor" problem led me down an investigation of the state of auto_ptr and its alternative, unique_ptr. I sought the wisdom of friends and people on the Freenode IRC. Wound up even building a newer version of gcc than came with the latest Kubuntu distribution! Though my findings are not the most exciting subject for a blog, I thought writing them up might help someone.
So I have good news, and bad news...

Bad News First

  • Unfortunately -- This appears to be caused by a problem in the C++ spec, and is not likely to ever be changed since std::auto_ptr is deprecated. (It's hard to imagine the ambiguity is the result of an oversight, but no one's offered a reason for it yet.)
  • Fortunately -- the new approved owned pointer class std::unique_ptr handles these implicit casts and is a must-have upgrade for reasons that are more important!
    Note For a skeptic's point of view, see what Bartosz Milewski has to say about unique_ptr. See also the answer I got from Bjarne Stroustrup directly when I asked him at a talk.
  • Unfortunately -- unique_ptr can't be implemented properly unless you have a version of the C++ compiler that supports rvalue references.
  • Fortunately -- rvalue references were added to gcc version 4.3, which is deployed fairly widely in today's linux distributions. (Try running g++ --version to see what you have...)
  • Unfortunately -- Just because you have a compiler that is able to compile the unique_ptr class doesn't mean you actually have unique_ptr in the standard library. (You need gcc 4.4 or later to get std::unique_ptr when you #include <memory>, AND you have to compile your program with the "-std=gnu++0x" command line option.)
  • Fortunately -- The boost interprocess library includes boost::interprocess::unique_ptr that you can use with older compilers.
  • Unfortunately -- It's hard to pin down what subset of unique_ptr's functionality it implements, and it doesn't seem to support any kind of casting (implicit -or- explicit).
  • Fortunately -- It's not that hard to build g++ 4.4 yourself if your distribution came with an earlier version...with some help from the interwebs.

"Didn't they teach you to shared_ptr in Kindergarten?"

These days most programmers have come to the consensus that there's a design flaw in auto_ptr. It uses the assignment operator to transfer ownership, while in almost every other programming context we assume that assignment makes an equivalent copy:
void Function(TypeX variableOfTypeX) {

    TypeX anotherVariableOfTypeX;

    // copy the contents of variableOfTypeX into 
    // anotherVariableOfTypeX
    anotherVariableOfTypeX = variableOfTypeX;

    if (anotherVariableOfTypeX != variableOfTypeX) {
       // We just assigned these to each other!
       ErrorMessage("What the heck is going on!?");
    }
}
After assigning one auto_ptr to another, you don't get a copy. Instead the source of the assign becomes NULL, while the target acquires the pointer. So the above psuedocode doesn't work.
This isn't a problem if you are writing code that is explicitly aware that it's working with auto_ptr types. But C++ seeks to empower the creation of generic objects, and it's a bummer if you have to have entirely different collection classes for managing owned pointers vs. regular pointers. unique_ptr patches the semantic inconsistency by forcing you to indicate moves explicitly in your code.
Yet rather than doing a knee-jerk upgrade from auto_ptr to unique_ptr, you might be better off using shared_ptr. Let's study a pretty good case I saw this week in the Qt Toolkit:
class QCoreApplication : (...) {
...
public:
void postEvent(QObject* receiver, QEvent* event);
// NOTE: The event must be allocated on the heap since the post 
// event queue will take ownership of the event and delete it once 
// it has been posted. It is not safe to modify or delete the event 
// after it has been posted.
...
};
Someone who thinks about design by contract might observe that the comment is doing nothing to let the compiler enforce the rule. Using unique_ptr can render the comment unnecessary:
class QCoreApplication : (...) {
...
public:
void postEvent(QObject* receiver, unique_ptr<QEvent>&& event);
...
};
Now the implicit contract is that you aren't storing any pointers to that QEvent after the call. You've passed the one remaining reference you had...and you're sure that no one else is holding onto one either. The compiler chips in some checking and enforcement, which is a much better state of affairs.
BUT it can be better to simplify the contract, instead of attempting to enforce it verbatim with the type system. If postEvent() took a shared_ptr, then it would free up the caller to post the same event multiple times. Or the code posting the event could just hang onto it, e.g. for logging purposes.
What makes this a good candidate for shared_ptr is that postEvent() doesn't change the expectations someone with a pointer to a QEvent might have had for what they can do with that pointer. It might be different with something that can be used only once, like digital cash:
class ZOnlineStore {
...
public:
void purchaseItem(ZInventoryItem item, unique_ptr<ZDigitalCash> cash);
...
};
It's not a perfect example, because a real system would need to be much more complex and transactional. I'm only suggesting that your problem should have some semantic uniqueness in its character. Otherwise, it's probably just going to add complexity to use the type system for helping follow the "hot potato" that a unique_ptr represents.
Note If you can achieve your smart pointer goals with shared_ptr semantics, notice that those are included in gcc 4.3 and also can handle the implicit upcast: sharedptrcast.cpp

autoptr to uniqueptr Transitional

Moving away from auto_ptr should be something everyone aims for. But if you hit this upcast ambiguity and are always casting to the same base type, perhaps you could consider a forward-looking macro like this:
// Base Class Move placeholder "BMOVE"
// Use this at call sites when a std::auto_ptr<Base> is expected
#define BMOVE(dptr) (static_cast< std::auto_ptr<Base> >(dptr))
This way, you could search and replace with move() when you switch to std::unique_ptr. Another alternative if you can't leap to a newer version of gcc is boost's pointer container library.

Notes on Building GCC Yourself

I managed to get gcc 4.4 to build inside a virtual machine running 64-bit kubuntu 9.04 with 512MB of allocated memory. The suggestion I got was to do this in my home directory. A few notes:
  • If you're short on memory, it might help you to kill X-Windows and your display manager during the build. How to do this varies from installation to installation, but I did it by logging into a virtual console (via Alt-F1) and killing the process for the display manager (called kdm). YMMV
  • If you only want a C++ compiler, you can speed the process and eliminate problems by telling configure --enable-languages='c++'
  • If you get some warnings about not being able to find 32 bit stubs, try setting the CFLAGS environment variable to -m64
  • If there's some message about problems with libc, then you might have to tell configure also to --disable-multilib
When you're all done with the build, do "which g++" and make sure that your current compiler version is in /usr/bin. That means the default make install will put your new version in /usr/local/bin. To get your personal profile to prefer the compiler in /usr/local, edit your .bashrc file and add:
export PATH="/usr/local/bin:$PATH"
People on the Freenode IRC channel #gcc were very helpful. And if you have a test case that you want to try in a newer compiler before you go through all of this, try out the geordi bot. It's very useful!
Business Card from SXSW
Copyright (c) 2007-2018 hostilefork.com

Project names and graphic designs are All Rights Reserved, unless otherwise noted. Software codebases are governed by licenses included in their distributions. Posts on blog.hostilefork.com are licensed under the Creative Commons BY-NC-SA 4.0 license, and may be excerpted or adapted under the terms of that license for noncommercial purposes.