-
Notifications
You must be signed in to change notification settings - Fork 3
Smart Pointers: Part 3
####Question 1: Consider the following code:
boost::shared_ptr<int> int1(new int(42));How many allocations were required? What was the size of each allocation?
####Answer 1:
For 32-bit VC10, two allocations were required, 4 bytes for the first, 16 bytes for the second.
For 64-bit VC10, two allocations were required, 4 bytes for the first, 24 bytes for the second.
There are two allocations for a single shared_ptr, one for the data being pointed to (what's returned by std::shared_ptr::get) and one for the reference count (so co-owning shared pointers know where to look to see how many other referrers there are).
####Exercise:
Create a int2 shared_ptr using make_shared.
auto int2 = std::make_shared<int>(42);####Question 2: How many allocations were required? What was the size of each allocation?
####Answer 2:
make_shared allocates the object pointed to and the reference count at the same time. Essentially, it creates one struct which holds both pieces of data. Thus:
For 32-bit VC10, one 16-byte allocation was required. For 64-bit VC10, one 24-byte allocation was required. If you used any other standard library, you probably got one 24-byte allocation (on 32-bit systems) or one 32-byte allocation (on 64-bit systems). Why are none of the sizes equal to the sum of the individual allocations? See below.
####BONUS Question 3:
Be assured that make_shared is allocating enough space for the same items allocated for int1. There should be something somewhat surprising about the sum of bytes allocated with make_shared versus the original version. Explain why the surprising result is okay.
####BONUS Answer 3: This was a harder question than I intended. First, if you're using standard library from anyone but Microsoft, you probably noticed that it allocated MORE than the sum of the individual allocations. This is due to how platforms pad their structs -- it's more efficient on most platforms to load aligned data. (In fact, some platforms crash unless this is the case!) Without delving into excessive detail, rather than allocating 20 bytes for a single struct holding the pointer and the reference count, the compiler allocates 24, so that the next struct is also aligned.
If that's true, then why does the MS version do what it does? The STL maintainer uses what he calls the "we know where you live optimization". See his presentation for details, but he essentially saves allocating a pointer by knowing that the allocated pointer was created via make_shared. This allows the make_shared struct to be one pointer smaller, which in turn allows the struct to be more compact, all without hurting alignment.
####Question 4: Consider the following code:
int foo() { throw std::runtime_error("Normally conditional"); return 5; }
void bar(std::shared_ptr<int> int, int val)
{}
void example2()
{
bar(std::shared_ptr<int>(new int(42)), foo());
}Why could calling example2 result in a memory leak?
####Answer 4:
C++ arguments may be evaluated in unspecified order. In the previous, the shared_ptr constructor and foo() must both be evaluated before bar is called, but there is no requirement that the shared_ptr constructor be called before foo() is evaluated. Thus the order of operations could be:
new int(42)
foo()
and the shared_ptr constructor might never be called, as foo() threw and exception.
This is explicitly warned about in the shared_ptr docs. So two solutions are:
std::shared_ptr<int> int(new int(42)); //Less freedom to reorder statements.
bar(int, foo());or
bar(std::make_shared<int>(42), foo());//No problems, make_shared won't be split to call foo().####Question 5:
std::make_shared always uses the std::allocator to retrieve memory. What should we use if we have a different allocator?
####Answer 5:
std::allocate_shared
####Question 6: Consider the following two ways to allocate an array:
void example3()
{
int* int1 = new int[42];
delete [] int1;
std::shared_ptr<int> int2(new int[42], [](int* p) { delete [] p; });
}Both have drawbacks. Using boost::shared_ptr as of 1.53 you can write:
boost::shared_ptr<int[]> int3(new int[42]);or
auto int4 = boost::make_shared<int[]>(42);//make_shared_noinit is actually functionally equivalent. . .The drawbacks to the code for int1 in example3 are obvious, since RAII classes protect against memory leaks. int2 has drawbacks that aren't quite as obvious. Name at least one drawback to storing an array in a shared_ptr.
####Answer 6:
Drawback #1: How do you access the second element of the array? int2[1] doesn't compile, as there's no overloaded operator[]. You end up having to write something like int2.get()[1], which is probably why languages without pointers are so popular these days. On the flip side, it can be argued that being able to access * and -> can lead to issues for arrays -- it might be better to require [0] instead.
Drawback #2:
The code for specifying an array deleter is verbose. It can be slightly prettier if you write std::default_delete<int[]>, but it's long enough to be a pain to type much.
Drawback #3 (subtle): Consider the following code
struct B {};
struct D : public B { std::array<int,2> vals };
std::shared_ptr<B> p_B(new D);//Okay. . .
std::shared_ptr<B> bs(new D[24]);//Awful!If you're not seeing what's wrong immediately, that's because it's subtle. In More Effective C++, Scott Meyers goes farther than his normal counsel to "avoid" things. He states "never treat arrays polymorphically." [^1] And he's right! Moving from one element to the next in an array requires pointer arithmetic. If each element is actually further apart than sizeof(B), then the pointer arithmetic will be wrong, and you won't access D[1] when you write pB[1].
Happily, with the new boost::shared_ptr type [^2] code like that above will fail to compile:
boost::shared_ptr<B[]> bs(new D[24]);//Compile error!while allowing other, safe conversions to take place:
boost::shared_ptr<const B[]> bs(new B[24]);//non-const to const okay. . .Using smart pointers can dramatically improve the safety of your code. It allows you to convey ownership semantics using something more than comments, while preventing memory leaks and potential double-deletions that come from improper memory leak fixes.
####Notes: [^1] See More Effective C++ item 3
[^2] http://www.boost.org/doc/libs/1_55_0/libs/smart_ptr/shared_ptr.htm