14

Sending the same pointer into two different shared_ptr's is bad, it causes double deallocation, like thus:

int* p = new int;
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p); // BAD

You can accomplish the same purpose with std::enable_shared_from_this:

class Good: public std::enable_shared_from_this<Good>
{
public:
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Good> gp1(new Good);
    std::shared_ptr<Good> gp2 = gp1->getptr();
}

But that still doesn't protect against:

class Good: public std::enable_shared_from_this<Good>
{
public:
    std::shared_ptr<Good> getptr() {
        return shared_from_this();
    }
};

int main()
{
    Good* p = new Good;
    std::shared_ptr<Good> gp3(p);
    std::shared_ptr<Good> gp4(p); // BAD
}

which could become a problem if you have code like this:

void Function(std::shared_ptr<Good> p)
{
    std::cout << p.use_count() << '\n';
}

int main()
{
    Good* p = new Good;
    std::shared_ptr<Good> p1(p);
    Function(p);    // BAD
}

Why would I use a regular pointer when there's smart pointers? Because in performance critical code (or for convenience) the overhead of shared_ptr or weak_ptr is undesirable.

To prevent this mistake, I've done:

class CResource : public shared_ptr<Good>
{
public:
    CResource()
    {
    }

    CResource(std::shared_ptr<CBaseControl> c)
        : CResource(c)
    {
    }

private:
    CResource(CBaseControl* p)
    {
    }
};

void Function(CResource p)
{
    std::cout << p.use_count() << '\n';
}

int main()
{
    Good* p = new Good;
    CResource p1(std::shared_ptr<Good>(p));
    Function(p);    // Error
}

This would cause the compiler to error if anyone tries to call Function with a pointer instead of a shared_ptr. It doesn't prevent someone from declaring void Function(std::shared_ptr p) though, but I think that's unlikely.

Is this still bad? Is there a better way of doing this?

Jorge Rodriguez
  • 603
  • 1
  • 6
  • 14
  • _"the overhead of `shared_ptr` or `weak_ptr` is undesirable"_ can you actually quantify this overhead? I'm involved in a project that does some pretty hefty image processing work with real-time requirements (eg. frame rate) and we make frequent use of `shared_ptr` because the *real* overhead is elsewhere in the algorithm. – Rook Jun 27 '12 at 10:18
  • By "undesirable overhead" I mean locking weak pointers, updating reference counts, stuff you don't have to do when you're dealing with raw pointers or references. – Jorge Rodriguez Jun 27 '12 at 10:29
  • 1
    But can you actually quantify that overhead? Have you run benchmarks and demonstrated that yes, the `std` managed pointers are too high a price to pay? I think you are engaging in premature optimisation. Or is it just that you don't like having to type in the extra calls to to lock each `weak_ptr` when you use it? – Rook Jun 27 '12 at 10:31
  • 1
    Colour me surprised. I'd be inclined to blame your system architecture, but that cannot be seen from here. – Rook Jun 27 '12 at 10:45
  • 3
    Your BAD example should not compile, shared_ptr has an `explicit` constructor so you can't pass a raw pointer to a function taking a `shared_ptr`. As Konrad's answer says, just don't write it in the obviously risky style. The question is basically "If I write good code it's safe, if I write bad code it's not." So don't do that. – Jonathan Wakely Jun 27 '12 at 11:07
  • @JonathanWakely: "Doctor, it hurts when I do this!" -- "Well, don't do that then!" – Christian Severin Jun 27 '12 at 11:55
  • You got the design completely wrong: you should *manage* resources with smart pointers, and offer interfaces with *raw* pointers --if the function does not keep ownership-- or const-references to the smart pointers --if the function needs to maintain ownership (i.e. stores it somewhere else. – David Rodríguez - dribeas Jun 27 '12 at 12:10
  • 1
    @JorgeRodriguez: You can get the performance of raw pointers if you use `boost::intrusive_ptr`, as it doesn't have an extra level of indirection. – user541686 Apr 01 '14 at 09:55
  • @Rook: You shouldn't be surprised, having an extra level of indirection is slower than not having it there. If you ask someone whether they've profiled and they say "yes", then you should probably believe them and move on instead of making it seem like there's something wrong with what they're doing. – user541686 Apr 01 '14 at 09:57
  • @Mehrdad, an object created by `std::make_shared` should have no additional overhead or indirection, and doesn't require modifying the class definition to derive from `intrusive_ptr` – Jonathan Wakely Apr 02 '14 at 12:04
  • @JonathanWakely: I believe `make_shared` does not get rid of the extra indirection, it only ensures the extra space is right next to the object in emmory. – user541686 Apr 02 '14 at 18:54
  • @Mehrdad, how is that different to having the reference counts embedded in the object? With `make_shared` you have `[refcount][obj]` and with `intrusive_ptr` you have `[refcount obj]` ... where's the indirection? – Jonathan Wakely Apr 03 '14 at 11:08
  • @Mehrdad, at least MSVC and GCC avoid storing a pointer from `[refcount]` to `[obj]` because it's at a known, fixed offset (what STL calls the "we know where you live" optimisation). That isn't required by the standard, but is an easy tweak, and unless I'm missing something it removes any advantage of `intrusive_ptr` – Jonathan Wakely Apr 03 '14 at 11:31
  • @JonathanWakely: The value inside `shared_ptr`, I believe, does not point to the object, and `shared_ptr` has no idea that the object necessarily follows the reference count (it may very well not). So getting the pointer to the object requires dereferencing that value. But the value inside `intrusive_ptr` does point to the object, so it removes that indirection. – user541686 Apr 03 '14 at 11:31
  • @Mehrdad, no, `shared_ptr` stores a pointer to the object, so accessing the object is a single deference, even if you don't use `make_shared` (the `shared_ptr` _also_ stores a pointer to the refcounts, which may or may not be adjacent to the object.) I've seen a few comments from you suggesting `boost::intrusive_ptr`, and I don't always agree that it's the right choice. – Jonathan Wakely Apr 03 '14 at 11:36
  • @JonathanWakely: I wasn't aware of that, but that brings up another point: `shared_ptr` is now twice as big as it needs to be. Which means it won't fit in a single register. It's still a potential performance bottleneck depending on the rest of the code, as that could affect register allocation and spilling. – user541686 Apr 03 '14 at 11:39
  • @JonathanWakely: Furthermore if I'm understanding it correctly, performing atomic operations on the `shared_ptr` now requires double-width atomic swaps which are not necessarily available right? – user541686 Apr 03 '14 at 11:40
  • @JonathanWakely: Whichever way you look at it, it has extra overhead compared to `intrusive_ptr`, there's no way around it. – user541686 Apr 03 '14 at 11:41
  • _"`shared_ptr` is now twice as big as it needs to be"_ No, it's exactly as big as it needs to be :) There's a _potential_ bottleneck, if you need it to be passed in a register. But not for the reasons you claimed above. It has trade-offs. A big advantage is you can use `shared_ptr` with any type, without adding a base class to your type. Atomic ops on `shared_ptr` do not require double word CAS, because they don't have to be lock-free. My main point is that your drive-by "use `intrusive_ptr`" comments are not always good advice, and your rationale was incorrect. – Jonathan Wakely Apr 03 '14 at 11:42
  • @JonathanWakely: I fully stand by my comment; it was perfectly good advice. The point was that **`shared_ptr` has more overhead than `intrusive_ptr`, and consequently, more than a raw pointer**. Either it has the indirection overhead I mentioned (an extra pointer per object, plus an extra dereference per access), or it has the one you mentioned (no extra access cost, but N extra pointers to handle per object, where N is the number of references to that object). Since the OP mentioned the performance problem, I thought it was worth mentioning, and I would do it again in the future. – user541686 Apr 03 '14 at 11:55
  • @JonathanWakely: And if you think double-word CAS is unnecessary because the implementation could use locks instead, I think you're entirely missing the point of the question and my comment (i.e., `shared_ptr` has performance overhead that `intrusive_ptr` doesn't have). @JonathanWakely: I didn't happen to know what particular implementation is typically used, so I made a mistake on assuming the implementation I thought would exist. You pointing out the mistake with the implementation assumption doesn't change my point one bit because it trades it off for a different overhead. – user541686 Apr 03 '14 at 11:58

2 Answers2

31

The solution is simple: never have a raw pointer own memory in the first place. This pattern:

int* p = new int;
std::shared_ptr<int> p1(p);
std::shared_ptr<int> p2(p); // BAD

Simply shouldn’t exist. Eradicate it from your code base. The only places where new is legitimate in C++11 is as an argument to a constructor call to a smart pointer (or in very low level stuff).

I.e. have code like this:

std::shared_ptr<int> p1(new int);

Or better yet (no naked new involved any longer):

auto p1 = std::make_shared<int>();

Note that it’s fine to use raw pointers in code (but I’d question even that in most C++ code I’ve seen). But if you use raw pointers, don’t let them own the memory. Either point to automatic storage (no resource management necessary) or use a unique_ptr and access the raw pointer via its get member function.

Konrad Rudolph
  • 530,221
  • 131
  • 937
  • 1,214
  • 3
    Good point. Having your ownership semantics clear is enough for your problem not to arise. Additionally, remember that when shared ownership isn't required, you can use `unique_ptr` with no overhead. – Kos Jun 27 '12 at 10:18
  • What if I write `std::shared_ptr p1(new int[N])`? Is this allowed? How would the smart pointer distinguish and handle this case? (I think you should add more stuffs in your answer). – Nawaz Jun 27 '12 at 10:18
  • 1
    @Nawaz you can supply a custom deleter as template parameter for `std::shared_ptr` that would call `delete[]` not `delete`. Unlike `unique_ptr` (where you can say `unique_ptr` there isn't one for `shared_ptr` by default for some reason, http://stackoverflow.com/questions/8947579/why-isnt-there-a-stdshared-ptrt-specialisation – Kos Jun 27 '12 at 10:23
  • And you can still simply do `shared_ptr>` which is a bit heavier but safe and flexible – Kos Jun 27 '12 at 10:25
  • Let's say I do: `void Function(int* p) { ... /* Doesn't store int anywhere */ } std::shared_ptr p1(new int); Function(p1.get());` Now `Function()` has a raw pointer and the problem persists. Sure, using the raw pointer is setting me up for this bad behavior, but let's just play the devil's advocate and say that it's necessary that I use a raw pointer in `Function()`. – Jorge Rodriguez Jun 27 '12 at 10:31
  • [Custom deleter example](http://stackoverflow.com/questions/627641/tr1-shared-arrays) on SO. I'd also prefer using `std::array` over `std::vector` where the length is known and fixed at compile time. – Rook Jun 27 '12 at 10:34
  • 2
    @Vino In itself, this doesn’t pose a problem. You can have a function that uses the raw pointer just fine. Simply don’t delete it anywhere, or use it to construct another smart pointer. – Konrad Rudolph Jun 27 '12 at 10:52
  • 2
    @Vino, using raw pointers to _point_ to an object is fine. Using raw pointers to _own_ an object is less good, use `shared_ptr` or `unique_ptr` or similar types to _own_ an object. If you need to transfer ownership to another function, pass the smart pointer to the function. – Jonathan Wakely Jun 27 '12 at 11:03
  • @Kos: I knew that already. I just wished the answer contains that information as well (that is why I asked the question). – Nawaz Jun 27 '12 at 11:07
  • I agree, I'm not using raw pointers to store/own an object. The problem is when trying to get a `shared_ptr` out of a raw pointer which already has a `shared_ptr` elsewhere, there's a right and a wrong way to do it, like `->shared_from_this()`. The question is, how to protect against the wrong ways? This reply is technically correct and well-formed, but I feel like it doesn't answer my question. – Jorge Rodriguez Jun 27 '12 at 11:11
  • 2
    @Vino: you can protect against the wrong ways by always passing a smart pointer around ;-) Alternative make it clear in your documentation that the consumer of the code is not to do their own memory management. That's as good as it gets with C++, once you've binned the other tools available to you. – Rook Jun 27 '12 at 11:18
  • @Vino I’d argue that you should simply never construct a `shared_ptr` from a raw pointer stored in an object. Of course, the fact that `enable_shared_from_this` exists in the first place contradicts this but I’m not convinced that its existence is beneficial. *But* I will admit that I might be overlooking something rather obvious. – Konrad Rudolph Jun 27 '12 at 11:58
  • _you can protect against the wrong ways by always passing a smart pointer around_ True, but this doesn't protect against a newbie or absent-minded programmer doing `C* p = sptr.get();` and then later `Function(p);` by mistake, with `Function(shared_ptr p)`. Edit: Except the explicit constructor does prevent that as Mr. Wakely points out in his answer. – Jorge Rodriguez Jun 27 '12 at 18:16
10

This example doesn't even compile:

void Function(std::shared_ptr<Good> p)
{
    std::cout << p.use_count() << '\n';
}

int main()
{
    Good* p = new Good;
    std::shared_ptr<Good> p1(p);
    Function(p);    // BAD
}

shared_ptr has an explicit constructor precisely to stop that happening.

To make that compile you need to write:

    Function( std::shared_ptr<Good>(p) );

which is pretty obviously wrong, and if someone's going to make that mistake they're just as likely to do:

    Function( CResource(std::shared_ptr<Good>(p)) );

So why have you bothered to write CResource? What does it add?

To expand on Konrad Rudolph's excellent answer:

The answer to your question of how to avoid problems is to follow the RAII idiom, but follow it fully.

We'll ignore the example that doesn't even compile and look at the one above it:

Good* p = new Good;
std::shared_ptr<Good> gp3(p);
std::shared_ptr<Good> gp4(p); // BAD

That code fails to follow the RAII idiom. You acquire a resource:

    Good* p = new Good;

But do not initialize an RAII type. BAD.

Then you initialize an object with some existing resource:

    std::shared_ptr<Good> gp3(p);

This is also BAD. You should initialize an RAII type at the same time as acquiring the resource, not separately (not even only separated by one line.)

Then you repeat the same error:

    std::shared_ptr<Good> gp4(p); // BAD

You've marked this line as "BAD" but actually the previous two lines were just as bad. The third line will cause undefined behaviour, but the first two lines allowed that error to creep in, when it should have been made more difficult. If you never had a Good* hanging around then you couldn't have used it to initialize gp4, you'd have needed to say shared_ptr<Good> gp4(gp3.get()) which is so obviously wrong noone will do it.

The rule is pretty simple: Don't take a raw pointer and put it in a shared_ptr. A raw pointer you didn't allocate is not resource acquisition so don't use it for initialization. The same applies inside Function, it should not take the raw pointer and use it to initialize a shared_ptr that takes ownership of the type.

This is C++, so it's not possible to protect against all the wrong ways to write code, you can't prevent sufficiently motivated idiots from shooting themselves in the foot, but all the guidelines and tools are there to prevent it, if you follow the guidelines.

Whn you have an interface that forces you to transfer ownership by passing a raw pointer, try to replace the interface so it uses a unique_ptr for ownership transfer, or if it's totally impossible to do that then try to wrap the interface in a safer version using unique_ptr for ownership transfer, and only as a last resort, use the dangerous interface but document it very explicitly that ownership is transferred.

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521