2

Context

I am trying to forward ownership of a std::unique_ptr through a lambda. But because std::functions must always be copyable, capturing a std::unique_ptr is not possible. I am trying to use a workaround described here: https://stackoverflow.com/a/29410239/6938024

Example

Here is my example which uses the described workaround

struct DataStruct
{
  /*...*/
};

class ISomeInterface
{
  /*...*/
};

class SomeOtherClass
{
public:
  void GetSomethingAsync(std::function<void(DataStruct)> resultHandler);
};

class MyClass
{
  void Foo(
    std::unique_ptr<ISomeInterface> ptr,
    std::function<void(std::unique_ptr<ISomeInterface>, DataStruct)> callback)
  {
    // I just want to "pass through" ownership of ptr
    std::shared_ptr sharedPtr = 
      std::make_shared<std::unique_ptr<ISomeInterface>>(std::move(ptr));

    mSomeOtherClass.GetSomethingAsync(
      [cb = std::move(callback), sharedPtr](DataStruct data)
      {
        auto unique = std::move(*sharedPtr.get());
        cb(std::move(unique), data);
      });
  }

  SomeOtherClass mSomeOtherClass;
};

Question

Does moving away the std::unique_ptr, which is managed by the std::shared_ptr cause any risk? My assumption would be, that the std::shared_ptr should try to delete the std::unique_ptr object, which is now dangling.

Is this code "safe"? Could the std::shared_ptr try to delete a dangling pointer, or can it know, that its managed object was moved away?

I would appreciate to understand the behaviour in this case more clearly. Someone please explain the specifics of what happens with the smart pointers here.

EDIT: Changed a wrong line in the example. cb(std::move(*sharedPtr), data); was changed to cb(std::move(unique), data);

mario.b
  • 165
  • 1
  • 12
  • 2
    There are no dangling pointers, but `auto unique = std::move(*sharedPtr.get())`, transfers ownership of the uniquely-owned object out of `sharedPtr` into `unique` (it does the same as `auto unique = std::move(*sharedPtr`)). After that, `*sharedPtr` owns nothing and the callback receives an empty `unique_ptr`. – molbdnilo Sep 28 '21 at 15:02

3 Answers3

1

Yes, it is safe. shared_ptr will eventually delete moved-away unique_ptr, but it is safe. Moved-away unique_ptr contains nullptr rather than a dangling pointer - this is what makes unique_ptr work in the first place.

Eugene
  • 6,194
  • 1
  • 20
  • 31
1

It feels to me me like you are trying to fit a solution to a problem. While your code looks fine (excepting threading issues, etc). The shared pointer will count how many references remain pointing to it, and will only delete the unique pointer once nothing is left that references it. However... it also looks over complicated. Remember that a std::function object can wrap any functor, not just a lambda. A lambda-function is just a light-weight way of expressing an object that is mostly a function. The shared_ptr is a work around for the short-comings of lambdas, so why use one?
The example struct PrintNum here is an example of a non-lambda object used in a std::function object. As long as your functor object supports the right move semantics, and a constructor that "moves" the pointer into the object, then no shared_ptr should be required.

IMHO.

EDIT: Eugene is correct, you will end up with the inherent problem that unique_ptr's are not copy able. You can still make your problem explicit by defining your own class.

For example, the code fragment below shows

  1. Using your own object with it's own function
  2. Using an object to simply adapt your own lambda
 1 #include <memory>
 2 #include <iostream>
 3 #include <functional>
 4
 5 using UPtr = std::unique_ptr<int>;
 6 using SPtr = std::shared_ptr<int>;
 7 // explicit conversion of unique to shared pointer
 8 class Adapter
 9 {
10    SPtr data;
11 public:
12    Adapter(UPtr&& d):data(std::move(d)) {}
13    void operator()() {std::cout << *data << "\n";}
14 };
15 // same but allow a (simple) lambda function
16 template<class Fn>
17 class FnAdapter
18 {
19    SPtr data;
20    Fn   fn;  // << note: uses copy semantics
21 public:
22    FnAdapter(UPtr&& d, Fn&& f):data(std::move(d)),fn(f) {}
23    void operator()() {fn(*data);}
24 };
25 int main()
27 {
28    UPtr data{new int{100}};
29    std::function<void()> fn{Adapter{std::move(data)}};
30    fn();
31
32    UPtr data2{new int{101}};
33    std::function<void()> fn2{FnAdapter{std::move(data2), [](int v) {std::cout << v << "\n";}}};
34    fn2();
35 }
36
Tiger4Hire
  • 1,065
  • 5
  • 11
1

We have our unique ptr to an interface I:

up[I]

where [] is ascii-art for the "box" we put the "I" in. We then put this in a box:

sp[up[I]]

the data is "double boxed".

We then pass this double box around. When we need to get at the unique ptr, we crack it with a std::move(*sp.get()). This leaves us with both of:

sp[up[nullptr]]
up[I]

a up[nullptr] is a perfectly ok smart pointer. It just has nothing inside the box. You can destroy such an empty box without a problem.

We then pass the up[I] into the callback.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Ah, sorry for the confusion. That was en error in the example. I edited the question to avoid further confusion. That explanation is very clear and makes sense to me. Thanks – mario.b Sep 28 '21 at 15:19