Search code examples
c++lambdaunique-ptr

Can I copy a `std::function` from a `std::unique_ptr` before moving it?


Consider the following hierarchy:

class BaseICannotChange {};

class DerivedIControl: public BaseICannotChange {
    private:
    int _Value;
    
    public:
    DerivedIControl(int value): _Value{value} {}
    int getValue () const {return _Value;}
};

I have a std::unique_ptr to a DerivedIControl. I want to retain the ability to get the value after the pointer has been moved, but while I am sure that the underlying object still exists:

int main () {
  auto myDerived = std::make_unique<DerivedIControl>(42);
  std::cout << "My value is: " << myDerived->getValue() << std::endl;
  std::unique_ptr<BaseICannotChange> myBase = std::move(myDerived);
  
  // This would crash as myDerived has been moved.
  // std::cout << "My value is: " << myDerived->getValue() << std::endl;
  return 0;
}

As I have control of DerivedIControl, is it legit to swap the getter for a std::function that I can then copy before moving the pointer?

class BaseICannotChange {};

class DerivedIControl: public BaseICannotChange {
    private:
    int _Value;
    
    public:
    DerivedIControl(int value): _Value{value} {}
    std::function<int(void)> getValue = [this] () {return _Value;};
};

Now I can write this:

int main () {
  auto myDerived = std::make_unique<DerivedIControl>(42);
  std::cout << "My value is: " << myDerived->getValue() << std::endl;
  auto myGetter = myDerived->getValue;
  std::unique_ptr<BaseICannotChange> myBase = std::move(myBase);
  std::cout << "My value is: " << myGetter() << std::endl;
  return 0;
}

I have tried this on a couple of online compilers, and it seems to work.

Is there a catch I am not seeing that makes it undefined behavior?


Solution

  • Yes, your code is valid. Moving a unique_ptr does not move the object it points to, so the captured this pointer in the std::function always remains valid. (To be pedantic: the only issue with your code is that BaseICannotChange needs a virtual destructor.)

    However, I think you're overthinking this. There's no reason to change the derived class's interface. Keep it simple and just store an extra reference to the object at the place where you need to.

    int main () {
      auto myDerived = std::make_unique<DerivedIControl>(42);
      std::cout << "My value is: " << myDerived->getValue() << "\n";
      auto &stillMyDerived = *myDerived; // get a non-owning reference to object
      std::unique_ptr<BaseICannotChange> myBase = std::move(myDerived); // and pass ownership to something else
      // object hasn't gone anywhere, this is fine
      std::cout << "My value is still: " << stillMyDerived.getValue() << "\n";
      return 0;
    }
    

    Note that you can't use a reference as a data member of a class (or, you can, but it makes a huge mess). Contrary to all the comments, I would not use std::unique_ptr::get to get a non-owning pointer in that case. This is not even to "avoid raw pointers", the issue is simply that a raw pointer is logically allowed to be nullptr, but it appears that you're not allowed to not have a valid reference in this case. So you should use a type that makes this clear:

    class TemporaryOwner {
        std::unique_ptr<DerivedIControl> ownership;
        std::reference_wrapper<DerivedIControl> object;
    public:
        TemporaryOwner(int value)
          : ownership(std::make_unique<DerivedIControl>(value))
          , object(*ownership)
          { }
        DerivedIControl &peekObject() { return object; }
        void printValue() { std::cout << "My value is: " << peekObject().getValue() << "\n"; }
        std::unique_ptr<BaseICannotChange> takeOwnership() { return std::move(ownership); }
    };
    // objects of this class become invalid if you take ownership and end up destroying the underlying object too early
    // it would be *safer* to use shared_ptr everywhere, but I would consider it okay to do it this way *if* you're careful