Jump to content

  • Log In with Google      Sign In   
  • Create Account


This article is under review by the community - Current moderation totals:
Mark as peer reviewed: 2 votes (Dave Hunt, jbadams)
Still needs work: 0 votes


Like
16Likes
Dislike

Smart Pointers Gotchas UNDER REVIEW

By Bartłomiej Filipek | Published Jun 04 2013 02:57 PM in General Programming

pointers smart pointers c++ c++11

There are at least several questions about using smart pointers in modern C++11
  • Why is auto_ptr deprecated?
  • Why does unique_ptr finally work good?
  • How to use arrays with unique_ptr?
  • Why create shared_ptr with make_shared?
  • How to use arrays with shared_ptr?
  • How to pass smart pointers to functions?
While learning how to use the new C++ standard I came across several issues with smart pointers. In general you can mess up a lot less using those helper objects and thus you should use them in your code instead of raw pointers. Unfortunately there are some topics you have to understand to take full advantage of them. As in most cases when you get a new tool to solve your problems, this tool introduces another problems as well.

Some predefines


Let us take a simple Test class with one member field to present further concepts:

class Test
{
public:
    Test():m_value(0) { std::cout << "Test::Test" << std::endl; }
    ~Test() { std::cout << "Test::~Test destructor" << std::endl; }

    int m_value;
};

typedef std::auto_ptr<Test>   TestAutoPtr;
typedef std::unique_ptr<Test> TestUniquePtr;
typedef std::shared_ptr<Test> TestSharedPtr;

Why is auto_ptr deprecated?


auto_ptr was one of the first type of smart pointers introduced in C++ (in C++98 to be more precise). It was designed to serve as a simple unique pointer (only one owner, without any reference counter), but people tried to use this also in a form of shared pointer. None of those functionalities were satisfied by auto_ptr's implementation! Quick example below:

void doSomethig(TestAutoPtr myPtr) {
    myPtr->m_value = 11;
}

void AutoPtrTest() {
    TestAutoPtr myTest(new Test());
    doSomethig(myTest);
    myTest->m_value = 10;
}

Try to compile and run this... what happens? It crashes just after we leave the doSomething procedure! We would assume than in doSomething some reference counter for our pointer is incremented, but auto_ptr has no such thing.

The object is destroyed because when we leave doSomething our pointer gets out of scope and it is deleted. To make it work we need to pass a reference to this auto pointer.

Another thing is that we have limited way of deleting more complicated objects, there is no control over it at all, only standard delete can be used here.

Why does unique_ptr finally work good?


Fortunately with the new standard we got a brand new set of smart pointers! When we change auto_ptr to std::unique_ptr<test> in our previous example we will get a compile (not runtime) error saying that we cannot pass a pointer to another function. And this is the proper behaviour.

unique_ptr is correctly implemented because of move semantics basically. We can move (but not copy) ownership from pointer to another. We also need to be aware when and where we pass the ownership. In our example we can use:

doSomethig(std::move(myTest));

to move the pointer's ownership. That way after the function returns our pointer is also not valid, but we did it on purpose after all. Another nice advantage of this type of pointer is that we can use custom deleters. It is useful when we have some complicated resources (files, textures, etc, etc).

How to use arrays with unique_ptr?


First thing to know:

std::unique_ptr<int> p(new int[10]);  // will not work!

The above code will compile, but when resources are about to be deleted only single delete will be called. So how do we ensure that delete[] is called? Fortunately unique pointers have a proper partial specialization for arrays and we can write:

std::unique_ptr<int[]> p(new int[10]);  
p[0] = 10; 

For our particular example:

std::unique_ptr<Test[]> tests(new Test[3]);

And we will get the desired output:

Test::Test
Test::Test
Test::Test
Test::~Test destructor
Test::~Test destructor
Test::~Test destructor


As expected :)

Note that if you want to pass address of the first element, you have to use &(pointerToArray[0]). Writing pointerToArray will not work.

Why create shared_ptr with make_shared?


Unique pointers provide their features only via wise usage of C++ syntax (using private copy constructor, assignment, etc), they do not need any additional memory. But with shared_ptr we need to associate some reference counter with our object. How to do that efficiently? When we do:

std::shared_ptr<Test> sp(new Test());
std::shared_ptr<Test> sp2 = std::make_shared<Test>();

We will get the output as expected:

Test::Test
Test::Test
Test::~Test destructor
Test::~Test destructor


So what is the difference? Why not use syntax similar to creation of unique_ptr? The answer lies in the allocation process. With the first construct we need to allocate a space for the object and then for the reference counter. With the second construct there is only one allocation (using placement new) and ref counter shares the same memory block as the pointed object.


Attached Image: smartPtr1.png
VS 2012 local's view


Above you can see a picture with local's view in the VS 2012. Compare the addresses of object data and reference counter block. For the sp2 we can see that they are very close to each other.

To be sure I got proper results I've even asked question on stackoverflow: http://stackoverflow.com/questions/14665935/make-shared-evidence-vs-default-construct

How to use arrays with shared_ptr?


Arrays with shared_ptr are a bit trickier that when using unique_ptr, but we can use our own deleter and have full control over them as well:

std::shared_ptr<Test> sp(new Test[2], [](Test *p) { delete [] p; });

We need to use custom deleter (here as a lambda expression). Additionally we cannot use make_shared construction.

Unfortunately using shared pointers for arrays is not so nice. I suggest taking boost instead. For instance: http://www.boost.org/doc/libs/1520/libs/smartptr/sharedarray.htm

How to pass smart pointers to functions?


We should use smart pointers as a first class objects in C++, so in general we should pass them by value to functions. That way reference counter will increase/decrease correctly. But we can use some other constructions which seems to be a bit misleading. Here is some code:

void testSharedFunc(std::shared_ptr<Test> sp) {
    sp->m_value = 10;
}

void testSharedFuncRef(const std::shared_ptr<Test> &sp) {
    sp->m_value = 10;
}

void SharedPtrParamTest() {
    std::shared_ptr<Test> sp = std::make_shared<Test>();

    testSharedFunc(sp);
    testSharedFuncRef(sp);
}

The above code will work as assumed, but in testSharedFuncRef we get no benefit of using shared pointers at all! Only testSharedFunc will increase reference counter.

For some performance critical code we, additionally, need to notice that passing by value will need to copy the whole pointer block, so maybe it is better to use even raw pointer there.

But perhaps the second option (with reference) is better? It depends. The main question is if you want to have full ownership of the object. If not (for instance you have some generic function that calls methods of the object) then we do not need ownership... simple passing by reference is a good and fast method.

It is not only me who got confused. Even Herb Sutter paid some attention to this problem and here is his post on that matter: http://herbsutter.com/2012/06/05/gotw-105-smart-pointers-part-3-difficulty-710/

Some additional comments


Smart pointers are very useful, but we, as users, also need to be smart :) I am not as experienced with smart pointers as I would like to be. For instance sometimes I am tempted to use raw pointers: I know what will happen, and at a time I can guarantee that it will not mess with the memory. Unfortunately this can be a potential problem in the future. When code changes my assumptions can be not valid any more and new bugs may occur. With smart pointers it is not so easy to break things.

All this topic is a bit complicated, but as usually in C++, we get something at a price. We need to know what we are doing to fully utilize the particular feature.

Code for the article: https://github.com/fenbf/review/blob/master/smart_ptr.cpp

Links

This article also is hosted on: www.codeproject.com


Reprinted with permission from Bartłomiej Filipek's blog



License


GDOL (Gamedev.net Open License)




Comments

But perhaps the second option (with reference) is better? It depends. The main question is if you want to have full ownership of the object.

 

I'd say the second option is always preferable. Reasoning:

 

- The caller has constructed an std::shared_ptr and has ownership of it, so the reference counter is guaranteed to be at least 1 for the whole duration of the method call

 

- If the called method wants to store the std::shared_ptr for later, everything still works: assign it to another std::shared_ptr (say, a field in the method's class) and it will increment the reference counter

 

- The first option would add a superfluous reference counter increment and decrement around the method.

 

Excellent article, by the way. Especially the information about std::make_shared() using placement new!

 

I think it might be useful - for programmers who already had contact with COM (or boost::intrusive_ptr) - to mention that constructing another std::shared_ptr to the same object would be a bad idea because it would create a second reference counter instead of just another reference. Also, std::shared_from_this and std::weak_ptr would be interesting topics :)

Passing by reference is better in my opinion.

 

Unless you want to create a "second owner" of the memory, you pass by reference.

Each active instance of a shared_ptr can be though of as one of the "owners" of the memory - but passing the pointer into a function shouldn't make the function an owner - it just modifies the pointer and moves on. References make more sense here. The counters, in my opinion, count owners of the memory, and thus shouldn't increment when handed to a function.

 

Const references for read-only access, and references for read-write access. Passing by value for creating a new owner. And it's more performance friendly - though that's hardly the motivating factor.

"Why does unique_ptr finally work good" should be "Why does unique_ptr finally work well"

 

/grammar Nazi

On the subject of passing by-reference versus by-value:

 

C++11 muddies the conventional wisdom here a bit with the introduction of move semantics. If you are using a C++11 compiler that supports them well, you may want to prefer passing by-value in some scenarios. 

 

Stephan Lavavej discusses this in his GoingNative 2012 presentation Magic && Secrets. The entire thing is worth a watch (it covers the make_shared optimization, for example), but the bit relevant to this discussion begins at 34:30 or so. He points out that passing by modifiable value is a reasonable option if you are going to simply copy the source to perform some mutation; this way you can avoid the copy when the parameter is a r-value because it can be moved into the parameter.

 

He summarizes that you should take your parameter by const-reference if you're going to observe the value, but if you're going to copy it anyway, you should consider taking it by modifiable value to take advantage of move semantics.

I feel it should be mentioned that if you want to dynamically allocate arrays you should probably use a std::vector to begin with. You can usually afford those two size_t for size and capacity...


Note: Please offer only positive, constructive comments - we are looking to promote a positive atmosphere where collaboration is valued above all else.




PARTNERS