8

I have a program that uses boost::shared_ptrs and, in particular, relies on the accuracy of the use_count to perform optimizations.

For instance, imagine an addition operation with two argument pointers called lhs and rhs. Say they both have the type shared_ptr<Node>. When it comes time to perform the addition, I'll check the use_count, and if I find that one of the arguments has a reference count of exactly one, then I'll reuse it to perform the operation in place. If neither argument can be reused, I must allocate a new data buffer and perform the operation out-of-place. I'm dealing with enormous data structures, so the in-place optimization is very beneficial.

Because of this, I can never copy the shared_ptrs without reason, i.e., every function takes the shared_ptrs by reference or const reference to avoid distorting use_count.

My question is this: I sometimes have a shared_ptr<T> & that I want to cast to shared_ptr<T const> &, but how can I do it without distorting the use count? static_pointer_cast returns a new object rather than a reference. I'd be inclined to think that it would work to just cast the whole shared_ptr, as in:

void f(shared_ptr<T> & x)
{
  shared_ptr<T const> & x_ = *reinterpret_cast<shared_ptr<T const> *>(&x);
}

I highly doubt this complies with the standard, but, as I said, it will probably work. Is there a way to do this that's guaranteed safe and correct?

Updating to Focus the Question

Critiquing the design does not help answer this post. There are two interesting questions to consider:

  1. Is there any guarantee (by the writer of boost::shared_ptr, or by the standard, in the case of std::tr1::shared_ptr) that shared_ptr<T> and shared_ptr<T const> have identical layouts and behavior?

  2. If (1) is true, then is the above a legal use of reinterpret_cast? I think you would be hard-pressed to find a compiler that generates failing code for the above example, but that doesn't mean it's legal. Whatever your answer, can you find support for it in the C++ standard?

AndyJost
  • 1,085
  • 1
  • 10
  • 18
  • Using the reference count in such a way looks like a *very* peculiar design route. Why not expose some control over whether an operation happens in-place or via copy to the user? Or separate shared and unique pointers? – Kerrek SB Apr 01 '12 at 00:46

4 Answers4

11

I sometimes have a shared_ptr<T> & that I want to cast to shared_ptr<T const> &, but how can I do it without distorting the use count?

You don't. The very concept is wrong. Consider what happens with a naked pointer T* and const T*. When you cast your T* into a const T*, you now have two pointers. You don't have two references to the same pointer; you have two pointers.

Why should this be different for smart pointers? You have two pointers: one to a T, and one to a const T. They're both sharing ownership of the same object, so you are using two of them. Your use_count therefore ought to be 2, not 1.

Your problem is your attempt to overload the meaning of use_count, co-opting its functionality for some other purpose. In short: you're doing it wrong.

Your description of what you do with shared_ptrs who's use_count is one is... frightening. You're basically saying that certain functions co-opt one of its arguments, which the caller is clearly using (since the caller obviously is still using it). And the caller doesn't know which one was claimed (if any), so the caller has no idea what the state of the arguments is after the function. Modifying the arguments for operations like that is usually not a good idea.

Plus, what you're doing can only work if you pass shared_ptr<T> by reference, which itself isn't a good idea (like regular pointers, smart pointers should almost always be taken by value).

In short, you're taking a very commonly used object with well-defined idioms and semantics, then requiring that it be used in a way that they are almost never used, with specialized semantics that work counter to the way everyone actually uses them. That's not a good thing.

You have effectively created the concept of co-optable pointer, a shared pointer that can be in 3 use states: empty, in use by the person who gave it to you only and thus you can steal from it, and in use by more than one person so you can't have it. It's not the semantics that shared_ptr exists to support. So you should write your own smart pointer that provides these semantics in a much more natural way.

Something that recognizes the difference between how many instances of a pointer you have around and how many actual users of it you have. That way, you can pass it around by value properly, but you have some way of saying that you are currently using it and don't want one of these other functions to claim it. It could use shared_ptr internally, but it should provide its own semantics.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • I agree; to allow reuse of temporaries, the correct way is to use move semantics in C++11 to determine whether a particular value (not `shared_ptr` to a value) is temporary and thus whether it is safe to overwrite. – Jeremiah Willcock Mar 31 '12 at 23:16
  • Unless I'm misunderstanding something I think you have the problem reversed. Taking the pointers by reference is what would break this use of `use_count`, whereas it should work when taking pointers by value. Also taking shared_ptrs by reference is common and recommended for certain circumstances. See my [answer](http://stackoverflow.com/a/9960608/365496). – bames53 Mar 31 '12 at 23:34
  • This answer is confusing the issue. The analogy to T * versus T const * is not correct; the correct analogy is to T * & and T const * &. – AndyJost Mar 31 '12 at 23:44
  • 3
    @AndyJost: That's kinda the point. When was the last time you used a *reference* to a pointer? Pointers, whether smart or stupid, are typically used by *value*, not reference. All these `shared_ptr&`s everywhere aren't a good idea. Either a function claims ownership of the memory (and thus should take a `shared_ptr` by value) or it *doesn't*, and thus should take a naked pointer. – Nicol Bolas Mar 31 '12 at 23:46
  • @bames: I don't think his addition works based on whether you move the last `shared_ptr` into the object (or just freshly allocated a temporary one). He doesn't claim C++11 usage, and he's using `boost::shared_ptr`. So it's likely that he's passing references around, expecting that `use_count==1` to mean that only the caller is using it and it's fine to co-opt it. I can't be certain, but that's my interpretation of his description.. – Nicol Bolas Mar 31 '12 at 23:53
  • @NicolBolas I wouldn't think that the case where only the caller is using it means it's fine to co-opt it. I'd expect that it's only fine to co-opt the object only if no one else is using it. But I suppose a program could choose different semantics, I'd just find it surprising. – bames53 Apr 01 '12 at 00:29
  • @bames53: I agree with finding it surprising, which is why I advised against it. It's just that without move semantics, the only way to pass a `shared_ptr` by value to a function and still have it only be owned by one user is for it to be a temporary. So I assume he has to be using it by reference. – Nicol Bolas Apr 01 '12 at 00:56
7

static_pointer_cast is the right tool for the job — you've already identified that.

The problem with it isn't that it returns a new object, but rather that it leaves the old object unchanged. You want to get rid of the non-const pointer and move on with the const pointer. What you really want is static_pointer_cast< T const >( std::move( old_ptr ) ). But there isn't an overload for rvalue references.

The workaround is simple: manually invalidate the old pointer just as std::move would.

auto my_const_pointer = static_pointer_cast< T const >( modifiable_pointer );
modifiable_pointer = nullptr;

It might be slightly slower than reinterpret_cast, but it's a lot more likely to work. Don't underestimate how complex the library implementation is, and how it can fail when abused.

An aside: use pointer.unique() instead of use_count() == 1. Some implementations might use a linked list with no cached use count, making use_count() O(N) whereas the unique test remains O(1). The Standard recommends unique for copy on write optimization.

EDIT: Now I see you mention

I can never copy the shared_ptrs without reason, i.e., every function takes the shared_ptrs by reference or const reference to avoid distorting use_count.

This is Doing It Wrong. You've added another layer of ownership semantics atop what shared_ptr already does. They should be passed by value, with std::move used where the caller no longer desires ownership. If the profiler says you're spending time adjusting reference counts, then you might add some references-to-pointer in the inner loops. But as a general rule, if you can't set a pointer to nullptr because you're no longer using it, but someone else might be, then you've really lost track of ownership.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
1

If you cast a shared_ptr to a different type, without changing the reference count, this implies that you'll now have two pointers to the same data. Hence, unless you erase the old pointer, you can't do this with shared_ptrs without "distorting the reference count".

I would suggest that you use raw pointers here instead, rather than going out of your way to not use the features of shared_ptrs. If you need to sometimes create new references, use enable_shared_from_this to derive a new shared_ptr to an existing raw pointer.

bdonlan
  • 224,562
  • 31
  • 268
  • 324
  • This is not the same as saying there are two pointers. This is two references to one pointer, where one of the references is type-spoofed. A doubt there is a safe way (hence the question), but it's frustratingly close, since the const qualifier probably doesn't change the layout or behavior of shared_ptr, and is disregarded when deleting the underlying object. – AndyJost Mar 31 '12 at 23:46
  • 1
    @AndyJost, there is no guarantee that adding the const won't change the layout or behavior of shared_ptr. If you're managing ownership manually (which is really what you're doing), just use raw pointers for passing to functions. – bdonlan Mar 31 '12 at 23:51
  • By the way, the recommendation to use raw pointers (actually, I would use references) is the correct thing to do, absent a safe way to cast the pointer. – AndyJost Mar 31 '12 at 23:54
  • the guarantee is the crux of the question. A priori, there is no such guarantee in the C++ standard. But the authors of shared_ptr _could_ make such a guarantee. If they did, it would be a great thing in this case. – AndyJost Mar 31 '12 at 23:59
  • 1
    @AndyJost, the creators of `shared_ptr` cannot make such a guarantee. You're casting between two different raw types, and the compiler does not provide a guarantee that `shared_ptr` has the same representation as `shared_ptr`, regardless of what the `shared_ptr` authors do. – bdonlan Apr 01 '12 at 04:09
  • That's just silly. The layout rules are spelled out with great precision (for C compatibility and many other reasons). Of course its possible to make sure shared_ptr and shared_ptr have the same layout in memory! – AndyJost Apr 01 '12 at 05:49
0

When it comes time to perform the addition, I'll check the use_count, and if I find that one of the arguments has a reference count of exactly one, then I'll reuse it to perform the operation in place.

This isn't necessarily valid unless you're applying some other rules across the whole program to make it so. Consider:

shared_ptr<Node> add(shared_ptr<Node> const &lhs,shared_ptr<Node> const &rhs) {
    if(lhs.use_count()==1) {
        // do whatever, reusing lhs
        return lhs;
    }
    if(rhs.use_count()==1) {
        // do whatever, reusing rhs
        return rhs;
    }
    shared_ptr<Node> new_node = ... // do whatever without reusing lhs or rhs
    return new_node;
}

void foo() {
    shared_ptr<Node> a,b;
    shared_ptr<Node> c = add(a,b);

    // error, we still have a and b, and expect that they're unchanged! they could have been modified!
}

Instead if you take the smart pointers by value:

shared_ptr<Node> add(shared_ptr<Node> lhs,shared_ptr<Node> rhs) {

And the use_count()==1 then you know that your copy is the only one and it should be safe to reuse it.

However, there's a problem in using this as an optimization, because copying a shared_ptr requires synchronization. It could well be that doing all this synchronization all over the place costs far more than you save by reusing existing shared_ptrs. All this synchronization is the reason it's recommended that code that does not take ownership of a shared_ptr should take the shared_ptr by reference instead of by value.

bames53
  • 86,085
  • 15
  • 179
  • 244
  • Copying `shared_ptr`s only requires atomic increments and decrements. And, as you point out, if you move them, then it doesn't even require that. So taking them by value is still fine. And if you don't intend to claim ownership of them, then you shouldn't be taking `shared_ptr`s *at all*. – Nicol Bolas Mar 31 '12 at 23:45
  • This analysis is true of any program... so what? – AndyJost Mar 31 '12 at 23:51
  • 1
    @NicolBolas Some code may take ownership conditionally, so it would be necessary to take a shared_ptr. Even atomic increments and decrements aren't free. [Here's](http://stackoverflow.com/a/8844924/365496) a snippet of what Herb Sutter and Scott Meyers had to say about it. – bames53 Mar 31 '12 at 23:55
  • 1
    @AndyJost I'm simply pointing out why you probably shouldn't be using `use_count()` for this. – bames53 Mar 31 '12 at 23:56
  • @bames53: I've seen that and I disagree with it. Yes, some functions conditionally take ownership, but how often does that really happen? Most functions that claim ownership are serious about it; the ones that aren't can take it by `const&` as they see fit. Plus, it aligns with the "always take `unique_ptr` by value" advice. It's easier to remember two rules than one. – Nicol Bolas Apr 01 '12 at 00:00
  • @NicolBolas If an object type is typically owned by a smart pointers I would probably elect to pass the smart pointer as a parameter even if a function doesn't take ownership. For example it may be easier to put a check in the function once than at every call site. Either way, my experience matches Herb Sutter's in that the vast majority of the time you want to avoid the shared_ptr copy, and I often do it by taking share_ptr by const & rather than by dereferencing the shared_ptr first. – bames53 Apr 01 '12 at 00:15
  • @bames53: So how do you differentiate between a function/object that does claim ownership vs. one that doesn't? One of the reasons some people give when they say not to take objects by non-const reference is so that you know when an object is being modified at the call site, without having to read the function to see if it's doing it. I'd say that this falls under a similar situation: if something takes a `shared_ptr`, then the user should assume that it takes ownership. If everyone takes `shared_ptr`s to these objects, then it's harder to track down the ownership graph. – Nicol Bolas Apr 01 '12 at 01:01
  • @NicolBolas Unlike the case against taking parameters by non-const reference, it should be irrelevant to the caller when the callee becomes a shared owner. The smart pointer is supposed to manage ownership, and otherwise it could be very problematic to handle those cases where ownership is taken conditionally. – bames53 Apr 01 '12 at 01:08
  • 1
    @bames53 Herb Sutter and Stephan Lavavej discuss the shared_ptr by-ref or by-value issue here too: http://channel9.msdn.com/Events/GoingNative/GoingNative-2012/Interactive-Panel-Ask-Us-Anything- and not everyone agrees that passing `shared_ptr` by-ref should be common. Personally, I agree with Nicol. If the code doesn't use any `shared_ptr` functionality, it should just take a `Node&` or `Node const&`, and the caller simply passes `*ptr`. Why limit your options by forcing the use of a `shared_ptr`? – R. Martinho Fernandes Apr 01 '12 at 01:15
  • @bames53: It may be irrelevant to the caller, but it's not irrelevant to the person who looks at the codebase and asks, "OK, who owns this object?" One of the points that Herb and Scott were making was that you shouldn't use `shared_ptr` *everywhere*; it denotes transfer of ownership, not "here, use this for a bit." `shared_ptr` is sometimes seen as a license to forget about ownership and memory management. And that path leads to memory leaks by circular reference, or just keeping stuff around longer than it needs to be. – Nicol Bolas Apr 01 '12 at 01:19
  • Btw, the discussion on the video I linked is at ~23 minutes. – R. Martinho Fernandes Apr 01 '12 at 01:28
  • @NicolBolas Well using a `shared_ptr` is not a license to not understand a program's ownership graph, but the way to understand ownership is not to walk though all the code and see who's passing what where. shared_ptr is a RAII type and seeing it passed should tell the reader exactly what seeing any RAII type passed would, namely that the local code is leaving the management of those resources up to the RAII object. Getting the bigger picture of who owns what and ensuring that the overall ownership graph is a DAG is left to other devices. – bames53 Apr 01 '12 at 01:55
  • @R.MartinhoFernandes Yeah, I've seen that as well and I agree with their comments there too. — Code that doesn't use the shared_ptr functionality may be overriding a function that takes a shared_ptr (perhaps another implementation does use the shared_ptr functionality). I'm also largely talking about types where the program typically always holds the object in a smart pointer, so it won't constitute a limitation. – bames53 Apr 01 '12 at 04:34