0

I was told that the only benefit of passing shared_ptr by value is sharing ownership, so I am trying to come up with example where we must pass shared_ptr by value rather than otherwise.

class Foo {
  public:
    void p() const {}
};

void bar(std::shared_ptr<Foo> f) {f->p();}
void bar2(const Foo* f) {f->p();}

int main() {
  std::shared_ptr<Foo> f = std::make_shared<Foo>();
  bar(f); // OK
  bar2(f.get()); // OK
}

The only example I can came up with seems to be a multiple thread environment where if f is created in one thread and passed to another thread, in that case it seems that shared_ptr is needed, otherwise the object may expire in the other thread.

Could someone provide a simple example where bar would work but bar2 would run into an issue in a single thread program? I am wondering if bar and bar2 will always work in a single thread program.

This could help me understand how to design proper APIs.


EDIT: Based on the reply, seems that my understanding of ownership was wrong. If I am designing some utility-like API, similar to bar/bar2, then I don't need to worry about ownership. Ownership makes sense when there are objects involved.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Zack
  • 1,205
  • 2
  • 14
  • 38
  • AFAIR this covers what is good practice for this case: https://youtu.be/JfmTagWcqoE - basically is there an ownership transfer or not? So what `bar` does or may do? – Marek R Mar 06 '23 at 15:56
  • 2
    a free function alone rarely takes ownership of something. More typically the function returns while for sure the shared object passed to the function is still alive. Consider `struct bar { std::shared_ptr x; };` and how its constructor would look like – 463035818_is_not_an_ai Mar 06 '23 at 15:57
  • FWIW if you can make yor function usable in a multithreaded context then you should probably do so. One day you might decide you need to use more threads and it helps to not have to rewrite a lot of other code to get that to work. – NathanOliver Mar 06 '23 at 16:03
  • q.v. [GotW 91](https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/) about Smart Pointers. – Eljay Mar 06 '23 at 19:52

1 Answers1

1

Sharing ownership has some relation to multithreading, but not as much as you seem to expect.

Sharing ownership is about sorting out who is responsible for releasing some resource when it is no longer needed. Smart pointers help you with the "when is it no longer needed?" part and the "releasing" part is a destructor call.

Suppose you have a class that manages some device, for example a printer. You want to share the same printer instance between different other instances making use of it, and when they are all done, the printer object should get destroyed. Then you can use a std::shared_ptr:

 struct foo {
    std::shared_ptr<printer> prtr;
 };

This is just to point you in the right direction. Now make the member private and write a constructor to initialize it from a std::shared_ptr passed from outside. That would be a case where you can consider to pass the smart pointer by value.

Your bar does not participate in ownership. Neither bar nor bar2 care how the objects lifetime is managed. It simply does not matter. The object is alive when the functions are called and it is alive when the functions returns. The functions do not hold on to a reference or pointer to the objects (they could store it in a function local static, but they don't).

When a function is not participating in ownership it should not take a std::shared_ptr, but either a reference or pointer, depending on whether nullptr is valid input. nullptr is no valid input for your functions, hence they should use a reference:

void bar(const Foo& f) {f.p();}

PS: I suggest you to consult the cpp core guidelines. I don't remember for sure, but I'd expect some guides for how to pass shared pointers.

463035818_is_not_an_ai
  • 109,796
  • 11
  • 89
  • 185
  • Is there any resource explaining how to design proper programs with clear ownership? Like some design guide for that? – Zack Mar 06 '23 at 16:20
  • @Zack https://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list – 463035818_is_not_an_ai Mar 06 '23 at 16:20
  • When you already have a smart pointer, degenerating it to a raw pointer is not wise. Use your `shared_ptr`, even when the ownership is not required. Don't create functions accepting or returning raw pointers, unless the input or output is forwarded from/to C API functions. – Red.Wave Mar 06 '23 at 16:30
  • @Red.Wave I cannot agree. If the function does not participate in ownership (and if nullptr is a valid input) then a raw pointer is fine. Why should the function require someone with a `Foo f` to create a shared pointer before being able to call it, when it would be ok to simply pass `f` or a (raw) pointer – 463035818_is_not_an_ai Mar 06 '23 at 16:40
  • @Red.Wave smart pointers are for ownership, there is no smart `dont_care_about_ownership_ptr` because we already have `Foo*` – 463035818_is_not_an_ai Mar 06 '23 at 16:41
  • @463035818_is_not_a_number No. Smart pointers are limiters on the dangerously high abstraction capabilities of raw pointers. The moment you start to use a raw pointer, safety issues to consider arise. It is wrong to expose the underlying pointer, unless a C API function needs it. – Red.Wave Mar 06 '23 at 17:44
  • 1
    @Red.Wave nothign is wrong with raw pointers. Smart pointers are a safe replacement for raw owning pointers that should be avoided. The pointers here are not owning pointers – 463035818_is_not_an_ai Mar 06 '23 at 18:19
  • 1
    See also: https://stackoverflow.com/q/68837288/5684257. – HTNW Mar 06 '23 at 20:28
  • @463035818_is_not_a_number none owning raw pointers are meant to communicate with C API functions. When the pointer is already embedded in a container/smart pointer and a C API function is not present, retrieving the raw pointer just means introducing runtime error risks. The only remaining case is to pass optional references; but in that case I'd reconsider the design; the parameter is supposed to be part of the return, to be ignored later, or opted with a flag input to the function. Safety os going to decide the faith of C++PL in the next few years. We need safety idioms. – Red.Wave Mar 06 '23 at 21:48
  • @Red.Wave I am merely suggesting the same as you can find in the [core guidelines F.7](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#f7-for-general-use-take-t-or-t-arguments-rather-than-smart-pointers): "A function that does not manipulate lifetime should take raw pointers or references instead. Passing by smart pointer restricts the use of a function to callers that use smart pointers." And irrespective of the guideline I think its a bad idea to make the function take a `shared_ptr`. I mean, why? It forces the caller to use a shared pointer even if they... – 463035818_is_not_an_ai Mar 06 '23 at 21:54
  • 1
    ...never meant to share ownership of the object or dynamically allocate it. I am not promoting the use of raw pointers. I am showing how to use a reference, and if `nullptr` is a valid input then they can use a raw pointer. They also can use `std::optional` but its a tradeoff to be made, `std::optional` is not a perfect replacement for a raw pointer. Though thats a discussion beyond the scope of this question. – 463035818_is_not_an_ai Mar 06 '23 at 21:55
  • @Red.Wave not using a raw pointer in this example adds no safety. I do not understand what you mean – 463035818_is_not_an_ai Mar 06 '23 at 22:05
  • @463035818_is_not_a_number those guidelines try to be as wide as possible. That's why raw pointers are mentioned besides references. As I pointed earlier, the only case where a C++ function needs a raw pointer input is optional references (something like `std::stoi`), in which case alternate designs should be considered. The problem with raw pointers is not just owning; accidental manipulation - due to their wide interface - is source of hard to trace bugs. You may simply index or offset a raw pointer and end up in a wrong address. – Red.Wave Mar 07 '23 at 10:30
  • @Red.Wave we are discussing over a sentence that mentions that if `nullptr` must be accepted then pointers can be used just to discard it immediately and suggest references instead. I see you got a point. Its an interesing discussion to be discussed, but not here, and I don't think I should edit the answer. Thanks for the feedback :) – 463035818_is_not_an_ai Mar 07 '23 at 20:37