6

Assume that I want to write function that takes in a pointer. However I want to allow caller to use naked pointers or smart pointers - whatever they prefer. This should be good because my code should rely on pointer semantics, not how pointers are actually implemented. This is one way to do this:

template<typename MyPtr>
void doSomething(MyPtr p)
{
    //store pointer for later use
    this->var1 = p;

    //do something here
}

Above will use duck typing and one can pass naked pointers or smart pointers. The problem occurs when passed value is base pointer and we need to see if we can cast to derived type.

template<typename BasePtr, typename DerivedPtr>
void doSomething(BasePtr b)
{
    auto d = dynamic_cast<DerivedPtr>(b);
    if (d) {
        this->var1 = d;

        //do some more things here
    }
}

Above code will work for raw pointers but won't work for the smart pointers because I need to use dynamic_pointer_cast instead of dynamic_cast.

One solution to above problem is that I add new utility method, something like, universal_dynamic_cast that works both on raw pointers and smart pointers by selecting overloaded version using std::enable_if.

The questions I have are,

  1. Is there a value in adding all these complexities so code supports raw as well as smart pointers? Or should we just use shared_ptr in our library public APIs? I know this depends on purpose of library, but what is the general feeling about using shared_ptr all over API signatures? Assume that we only have to support C++11.
  2. Why doesn't STL has built-in pointer casts that are agnostic of whether you pass raw pointers or smart pointers? Is this intentional from STL designers or just oversight?
  3. One other problem in above approach is loss of intellisense and bit of readability. This is the problem obviously in all duck typed code. In C++, however, we have a choice. I could have easily strongly typed my argument above like shared_ptr<MyBase> which would sacrifice flexibility for callers to pass whatever wrapped in whatever pointer but reader of my code would be more confident and can build better model on on what should be coming in. In C++ public library APIs, are there general preferences/advantages one way or another?
  4. There is one more approach I have seen in other SO answer where the author proposed that you should just use template<typename T> and let caller decide if T is some pointer type or reference or class. This super generic approach obviously don't work if I have to call something in T because C++ requires dereferencing pointer types which means I have to probably create utility method like universal_deref using std::enable_if that applies * operator to pointer types but does nothing for plain objects. I wonder if there are any design patterns that allows this super generic approach more easily. Again, above all, is it worth going all these troubles or just keep thing simple and use shared_ptr everywhere?
Community
  • 1
  • 1
Shital Shah
  • 63,284
  • 17
  • 238
  • 185
  • `(*p)->someMember()` That's one dereference too many. I think you mean `p->someMember()` or, if for some reason you want to be extra verbose, `(*p).someMember()`. – Igor Tandetnik Dec 20 '16 at 01:26
  • @IgorTandetnik - oops, sorry. Fixed. – Shital Shah Dec 20 '16 at 01:28
  • Seems like a lot of complexity designed just to cause confusion. Make your API take one known type and stick with it! – John3136 Dec 20 '16 at 01:28
  • 2
    You can generally get to the underlying raw pointer with `&*b`, or `std::addressof(*b)` to be extra pedantic. That works for raw pointers, standard smart pointers, and iterators, at least. Then `dynamic_cast` that raw pointer to your heart's content. – Igor Tandetnik Dec 20 '16 at 01:28
  • 2
    You keep using the word "universal", but you clearly only have raw pointers and `std::shared_ptr` in mind. E.g. I don't see how your hypothetical `universal_dynamic_cast` would work with `std::unique_ptr`, where you can't have two instances holding pointers to the same object. – Igor Tandetnik Dec 20 '16 at 01:39
  • @IgorTandetnik I think I'm more thinking about just assuming pointer semantics, not implementation. Someone can probably design different pointer-like object and pass to me. However you are in correct that this is quite a stretch and alll my use cases will involve either raw pointers or shared_ptr as far as I can see. – Shital Shah Dec 20 '16 at 01:43
  • If all `doSomething` needs to do is invoke a member function, why is it taking a `shared_ptr` at all? Why not `template void doSomething(Base *b)`? – Praetorian Dec 20 '16 at 01:46
  • @Praetorian Perhaps I have simplified too much here. Assume that method is member of class can store the pointer to use later. – Shital Shah Dec 20 '16 at 01:53
  • @ShitalShah: "*this->var1*" `doSomething` is not a member function of some type. – Nicol Bolas Dec 20 '16 at 04:00

5 Answers5

4

To store a shared_ptr within a class has a semantic meaning. It means that the class is now claiming ownership of that object: the responsibility for its destruction. In the case of shared_ptr, you are potentially sharing that responsibility with other code.

To store a naked T*... well, that has no clear meaning. The Core C++ Guidelines tell us that naked pointers should not be used to represent object ownership, but other people do different things.

Under the core guidelines, what you are talking about is a function that may or may not claim ownership of an object, based on how the user calls it. I would say that you have a very confused interface. Ownership semantics are usually part of the fundamental structure of code. A function either takes ownership or it does not; it's not something that gets determined based on where it gets called.

However, there are times (typically for optimization reasons) where you might need this. Where you might have an object that in one instance is given ownership of memory and in another instance is not. This typically crops up with strings, where some users will allocate a string that you should clean up, and other users will get the string from static data (like a literal), so you don't clean it up.

In those cases, I would say that you should develop a smart pointer type which has this specific semantics. It can be constructed from a shared_ptr<T> or a T*. Internally, it would probably use a variant<shared_ptr<T>, T*> or a similar type if you don't have access to variant.

Then you could give it its own dynamic/static/reinterpret/const_pointer_cast functions, which would forward the operation as needed, based on the status of the internal variant.

Alternatively, shared_ptr instances can be given a deleter object that does nothing. So if your interface just uses shared_ptr, the user can choose to pass an object that it technically does not truly own.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • You do bring some clarity here however the problem is that you can't really tell if caller would always wants you to assume ownership or not. Some simple callers may be using T* and don't want to construct shared_ptr for you while other callers may be using shared_ptr all over exclusively because they don't want to get in the business of tracking ownership. This dilemma is more for library developers whose classes could be used in variety of applications. I have seen some applications which have guidelines never to use naked pointers anywhere. While some simple small apps do it all the time. – Shital Shah Dec 20 '16 at 04:17
  • So what I'm trying to say: there are two kind of ownership models applications use: One is centralized owner who hands out T* to everyone else and other is decentralized model where only shared_ptr is passed around, circular references are prohibited and naked pointers are avoided like plague. As a library developer you don't know what model your calling app would be using. So there is some argument for developing this generic interface. – Shital Shah Dec 20 '16 at 04:27
  • @ShitalShah: "*while other callers may be using shared_ptr all over exclusively because they don't want to get in the business of tracking ownership*" That's not what `shared_ptr` does. `shared_ptr` is not the same thing as garbage collection; it doesn't allow you to *ignore* ownership relationships. I wouldn't want to facilitate users making poor coding decisions. "*As a library developer you don't know what model your calling app would be using.*" As a library developer, you must make decisions based on what your library is *doing*. And claiming ownership or not is part of that. – Nicol Bolas Dec 20 '16 at 15:25
2

The usual solution is

template<typename T>
void doSomething(T& p)
{
    //store reference for later use
    this->var1 = &p;
}

This decouples the type I use internally from the representation used by the caller. Yes, there's a lifetime issue, but that's unavoidable. I cannot enforce a lifetime policy on my caller and at the same time accept any pointer. If I want to ensure the object stays alive, I must change the interface to std::shared_ptr<T>.

MSalters
  • 173,980
  • 10
  • 155
  • 350
1

I think the solution you want is to force callers of your function to pass a regular pointer rather than using a template function. Using shared_ptrs is a good practice, but provides no benefit in passing along the stack, since the object is already held in a shared pointer by the caller of your function, guaranteeing it does not get destroyed, and your function isn't really "holding on" to the object. Use shared_ptrs when storing as a member (or when instantiating the object that will become stored in a member), but not when passing as an argument. It should be a simple matter for the caller to get a raw pointer from the shared_ptr anyway.

John Thoits
  • 355
  • 1
  • 12
  • I think I tried too much to simplify scenario here. Assume that we are designing class interface and methods can store the passed pointers to member variables. – Shital Shah Dec 20 '16 at 01:55
  • 2
    @ShitalShah: Then those "class interface and methods" need to know *what kind* of pointers they store. An interface that stores a `unique_ptr` is going to look a lot different than one that stores a `shared_ptr`. And even that's going to be different from one that stores a `T*`. You should not try to design such interfaces which are blind to what they are storing. – Nicol Bolas Dec 20 '16 at 02:07
  • 1
    @ShitalShah You might consider using shared_ptrs only in your functions that store into members then. Without knowing more, I think you might also consider whether you really need to share ownership of the objects, or if you might work your design so that shared ownership is not needed. shared_ptrs are a great tool, but not always (and in my experience most often not) the correct tool. – John Thoits Dec 20 '16 at 03:24
  • @NicolBolas I think T* and shared_ptr shared same semantics and so should be interchangeable. I agree unique_ptr would need different interface. For methods that don't save pointers, T* might suffice and that's simple scenario however my question is more around how do we generalize on T* and shared_ptr assuming that we are storing it within the class. Is it acceptable to have public library class interface to be sprikled with shared_ptr? You obviously can't always bet if caller will always assume ownership in general scenario. – Shital Shah Dec 20 '16 at 03:55
  • @JohnThoits Yes, the question is about the general scenario where we might store the pointer in member variable. I'll add this in to question. – Shital Shah Dec 20 '16 at 03:56
  • @ShitalShah: "*I think T* and shared_ptr shared same semantics*" No, they don't. One represents *ownership*: the right to destroy the object. The other does not. It is extremely rare for an interface to maybe *or maybe not* claim ownership of an object. And if I were to have such a need, I'd rather wrap that possible-ownership concept in its own type. – Nicol Bolas Dec 20 '16 at 03:59
  • @NicolBolas You have a point. By your argument, may be one shouldn't attempt to design interface to accept generic pointer-like types and should always enforce specific type required by interface? I can see that T* is more general in the sense you can easily go from shared_ptr to T* but not vice versa. But on the other hand, caller may needs to go extra step to ensure the pointer survives for the life span of some *other* object. – Shital Shah Dec 20 '16 at 04:04
1

The purpose of smart pointers

The purpose of smart pointers is to manage memory resources. When you have a smart pointer, then you usually claim unique or shared ownership. On the other hand, raw pointers just point to some memory that is managed by someone else. Having a raw pointer as a function parameter basically tells the caller of the function that the function is not caring about the memory management. It can be stack memory or heap memory. It does not matter. It only needs to outlive the lifetime of the function call.

Semantics of pointer parameters

When passing a unique_ptr to a function (by value), then your passing the responsibility to clean up memory to that function. When passing a shared_ptr or weak_ptr to a function, then that's saying "I'll possibly share memory ownership with that function or object it belongs to". That's quite different from passing a raw pointer, which implicitly mean "Here's a pointer. You can access it until you return (unless specified otherwise)".

Conclusion

If you have a function, then you usually know which kind of ownership semantics you have and 98% of the time you don't care about ownership and should just stick to raw pointers or even just references, if you know that the pointer you're passing is not a nullptr anyways. Callers that have smart pointers can use the p.get() member function or &*p, if they want to be more terse. Therefore, I would not recommend to template code to tackle your problem, since raw pointers give the caller all the flexibility you can get. Avoiding templates also allows you to put your implementation into an implementation file (and not into a header file).

To answer your concrete questions:

  1. I don't see much value in adding this complexity. To the contrary: It complicates your code unnecessarily.

  2. There is hardly any need for this. Even if you use std::dynamic_pointer_cast in the such, it is to maintain ownership in some way. However, adequate uses of this are rare, because most of the time just using dynamic_cast<U*>(ptr.get()) is all you need. That way you avoid the overhead of shared ownership management.

  3. My preference would be: Use raw pointers. You get all the flexibility, intellisense and so forth and you will live happily ever after.

  4. I would rather call this an antipattern - a pattern that should not be used. If you want to be generic, then use raw pointers (if they are nullable) or references, if the pointer parameter would never be a nullptr. This gives the caller all the flexibility while keeping the interface clean and simple.

Further reading: Herb Sutter talked about smart pointers as function parameters in his Guru of the Week #91. He explains the topic in depth there. Especially point 3 might be interesting to you.

Ralph Tandetzky
  • 22,780
  • 11
  • 73
  • 120
0

After reviewing some more material, I've finally decided to use plain old raw pointers in my public interface. Here is the reasoning:

  1. We shouldn't be designing interface to accommodate bad design decisions of others. The mantra of "avoid raw pointers like a plague and replace them with smart pointers everywhere" is just bad advice (also se Shutter's GoTW). Trying to support those bad decisions spreads them in to your own code.
  2. Raw pointers explicitly sets up contract with callers that they are the one who need to worry about lifetime of inputs.
  3. Raw pointers gives the maximum flexibility to callers who have shared_ptr, unique_ptr or just raw pointers.
  4. Code now looks much more readable, intuitive and reasonable unlike those duck typed templates taking over everywhere.
  5. I get my strong typing back along with intellisense and better compile time checks.
  6. Casting up and down hierarchy is a breeze and don't have to worry about perf implications where new instance of smart pointer may get created at each cast.
  7. While passing pointers around internally, I don't have to carefully care if the pointer would be shared_ptr or raw pointer.
  8. Although I don't care about it, there is better pathway to support older compilers.

In short, trying to accommodate potential clients who have taken up on guidelines of never using raw pointers and replace them with smart pointers everywhere causes polluting my code with unnecessary complexity. So keep simple things simple and just use raw pointers unless you explicitly want ownership.

Community
  • 1
  • 1
Shital Shah
  • 63,284
  • 17
  • 238
  • 185
  • "*Casting up and down hierarchy is a breeze and don't have to worry about perf implications where new instance of smart pointer may get created at each cast.*" Considering the cost of a `dynamic_cast`, I don't think the creation of a new `shared_ptr` is going to compare to that. Indeed, I have to question why you need to be "casting up and down hierarchy" with any great frequency. – Nicol Bolas Dec 20 '16 at 15:28
  • I have container of polymorphic objects stored as Base*. The action taken on these objects depends on what level of interface in the hierarchy they support. For example, you can have container of Shape* and actual objects may be Rectangle, Square (derived from Rectangle), Oval, Circle (derived from Oval). Then you might have method that makes all ovals green. – Shital Shah Dec 20 '16 at 20:10
  • 1
    Now you're [violating OOP principles](http://stackoverflow.com/q/56860/734069). If you have a `Base*`, then you have chosen *not to care* if it is any of its derived classes. You're saying that any class derived from `Base*` can be used adequately by that interface. If you have a container of `Base`s, then you should limit your operation to only the things that `Base`s can do. `dynamic_cast` should not be a common element of your code; it should only be used in *exceptional* circumstances. Consider [these answers](http://stackoverflow.com/q/35764905/734069). – Nicol Bolas Dec 20 '16 at 20:27