0

So in modern C++ we use smart pointers to make sure that a shared resource didn't get deleted before we go to use it.

Is there an equivalent for std::function<> for lambda callbacks? Where you pass out a function of a certain signature rather than a whole class or interface, general delegate injection.

Example code of problem:

struct Foo{
    int i = 0;
    void addInt(int input){
        i += input;
    }
};

int main(int argc, char* args[]){
    Foo* lostMemory = new Foo();
    std::function<void(int)> lambda = [lostMemory](int b) { lostMemory->addInt(b); };

    //run memory good
    lambda(8);

    //run memory deleted - should crash right?
    delete lostMemory;
    lambda(8);
    return 0;
}

https://godbolt.org/z/qsK65ozGG

The fact it didn't crash isn't the problem or question

Question: What is the right way to create a std::functional like this, where the resource may be deleted?

Note: in an example where lostMemory will be an instance of a class passing itself, and can't just be changed to a std::shared_ptr<>. Stack overflow simplification makes it look like that is the answer.

Natio2
  • 235
  • 1
  • 9
  • The equivalent of std::function for lambdas is std::function. This program has undefined behaviour because it's the same as doing `delete lostMemory; lostMemory->i += 8;` Undefined behaviour crashes sometimes but not always. – user253751 Feb 25 '23 at 02:08
  • 1
    Welcome to C++. Yes, today when you run this program it did not crash. But it'll probably crash tomorrow, or next Thursday. You never know. This is undefined behavior. Anything can happen. Including no crash. Because "no crash" meets the technical requirements for "anything". It's also quite unclear how "manage memory" is related to "pass out a function of a certain signature". One has nothing to do with the other. – Sam Varshavchik Feb 25 '23 at 02:08
  • 1
    **Answer:** Don't delete the resource before you use it. Doesn't matter whether you use std::function or not. Doesn't matter whether you use lambdas or not. Your job is to not use things after you delete them. – user253751 Feb 25 '23 at 02:15
  • @user253751 I guess there's no need for std::shared_ptr<> either since we will always perfectly know the state of memory... I'm looking for the RAII solution – Natio2 Feb 25 '23 at 02:18
  • @Natio2 yes actually, shared_ptr is useful but it does not magically make everything work. You can use shared_ptr here if you want to. And it does solve your problem. But you are not using it. – user253751 Feb 25 '23 at 02:19
  • 2
    "_lostMemory will be an instance of a class passing itself, and can't just be changed to a std::shared_ptr<>_" That's what [`std::enable_shared_from_this`](https://en.cppreference.com/w/cpp/memory/enable_shared_from_this) is for, but keep in mind you'll need to take precautions to make sure every instance of the class is owned by a `shared_ptr` for it to work. – Miles Budnek Feb 25 '23 at 02:19
  • Are you asking how to make it so that `delete lostMemory;` doesn't actually delete `lostMemory`? – user253751 Feb 25 '23 at 02:20
  • @MilesBudnek Thanks, this is the sort of solution I'm looking for! I'll go read the APi docs for std::enable_shared_from_this. – Natio2 Feb 25 '23 at 02:22
  • 1
    @Natio2 It allows you to get a shared_ptr from an instance of the class, but only if the class is already being managed by shared_ptr, otherwise it's undefined behaviour. – user253751 Feb 25 '23 at 02:25
  • 1
    @Natio2 An object can't hold a `shared_ptr` to itself because then it's never going to get deleted. (And it's also not trivial to *get* that `shared_ptr` into the object!) If you use `shared_from_this` then you basically have to `private` the constructor and use factory `static` functions (returning `shared_ptr`). The class no longer supports value semantics and instead has the mandatory reference semantics you'd see in Java. – HTNW Feb 25 '23 at 02:32
  • @HTNW that makes sense. I guess the answer is I need to rearrange my object stack so a class is never passing itself. – Natio2 Feb 25 '23 at 02:38
  • Are you asking how to make it so that delete lostMemory; doesn't actually delete lostMemory? – user253751 Feb 25 '23 at 03:29
  • @user253751 I wrote an answer, but can't post cause someone marked duplicate and linked unrelated topics. My solution I create a struct with the function and a weak_ptr lock which it would check before using the function. Deals with the object can't have it's own shared_ptr problem – Natio2 Feb 25 '23 at 04:29
  • I agree that is not a duplicate because your question is related to managing the object lifetime, and not related to why the undefined behaviour doesn't crash. And I have the reopen-hammer power for C++ questions, apparently, so I have reopend it. – user253751 Feb 25 '23 at 04:31
  • Note that weak_ptr has the same concern: the object has to be managed by shared_ptr. You can't make a weak_ptr for any old object - you have to make one from a shared_ptr – user253751 Feb 25 '23 at 04:31
  • @user253751 haha nice. Uh I'll make an answer post, and you can see how horrible it is. It's still a bit jank, but a bit safer – Natio2 Feb 25 '23 at 04:33
  • std::shared_from_this basically works by using weak_ptr – user253751 Feb 25 '23 at 04:35
  • @user253751 Answer posted, tell me what you think – Natio2 Feb 25 '23 at 04:53
  • @Cristik it does cover some of these comments, but it actually ignores the big problem with using shared_from_this(), as discussed here. Such as that first block creating the shared pointer on lambda call doesn't actually help, unless the memory is already managed, so not sure the code suggested in that thread would actually work, I prefer my answer which is a little less magic, but will work. – Natio2 Feb 25 '23 at 06:02
  • @Natio2 recommending then to update the question to make it more clear it asks about `shared_from_this` instead of general memory management for lambda captures. – Cristik Feb 25 '23 at 06:08
  • @Cristik but it is about memory management, just the thread you suggested says to use shared_from_this() as RAII memory management of lambdas, but to me it looks incorrect. My answer (Below) does not use shared_from_this() but manages the memory of lambdas – Natio2 Feb 25 '23 at 06:09
  • OK, if the questions are the same, then the best course of actions would be to post your answer to the other question. Especially if you think the answers there are incorrect/incomplete. – Cristik Feb 25 '23 at 06:16
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/252120/discussion-between-natio2-and-cristik). – Natio2 Feb 25 '23 at 06:21

1 Answers1

2

Why don't you just use smart pointers as you mentioned?


#include <functional>
#include <memory>
#include <iostream>

struct Foo{
    Foo() {
        std::cout << "Foo created" << std::endl;
    }
    ~Foo() {
        std::cout << "Foo destroyed" << std::endl;
    }
    int i = 0;
    void addInt(int input){
        i += input;
    }
};

int main(int argc, char* args[]){
    std::cout << "-- Start of program" << std::endl;
    std::shared_ptr<Foo> lostMemory( new Foo() );
    std::weak_ptr<Foo> weak = lostMemory;
    std::cout << "-- Before lambda" << std::endl;
    std::function<void(int)> lambda = [weak](int b) { 
        if ( auto sp = weak.lock() ) { 
            std::cout << "Added int" << std::endl;
            sp->addInt(b);
        } 
        else std::cout << "Not doing this" << std::endl;
    };
    std::cout << "-- After lambda" << std::endl;
    lambda(8);
    std::cout << "-- Before deleting" << std::endl;
    lostMemory.reset();
    std::cout << "-- After lambda" << std::endl;
    lambda(8);
    std::cout << "-- The end" << std::endl;
    return 0;
}

Produces

Program stdout
-- Start of program
Foo created
-- Before lambda
-- After lambda
Added int
-- Before deleting
Foo destroyed
-- After lambda
Not doing this
-- The end

Godbolt link: https://godbolt.org/z/ceMevznhv

See, lambdas are mere anonymous functors under the hood. If you paste the above code on cppinsights.io you will have something like this (cleaned up a bit):

#include <functional>
#include <memory>

struct Foo
{
  int i = 0;
  inline void addInt(int input)
  {
    this->i = this->i + input;
  }
};

int main(int argc, char ** args)
{
  std::shared_ptr<Foo> lostMemory = std::shared_ptr<Foo>(new Foo());
    
  class __lambda_13_39
  {
    public: 
    inline /*constexpr */ void operator()(int b) const
    {
      static_cast<const std::__shared_ptr_access<Foo, 2, false, false>&>(lostMemory).operator->()->addInt(b);
    }
    
    private: 
    std::shared_ptr<Foo> lostMemory;
    public: 
    __lambda_13_39(const std::shared_ptr<Foo> & _lostMemory)
    : lostMemory{_lostMemory}
    {}
    
  };
  
  std::function<void (int)> lambda = std::function<void (int)>(__lambda_13_39{lostMemory});
  lambda.operator()(8);
  static_cast<std::__shared_ptr<Foo, 2>&>(lostMemory).reset();
  lambda.operator()(8);
  return 0;
}

You can see that the functor holds an std::shared_ptr<Foo> as a member. That is exactly what the lambda will do as well.

Alternatively, use a functor itself instead of a lambda for cases that are a bit more evolving.

Something Something
  • 3,999
  • 1
  • 6
  • 21
  • Like this is the idea case where you can turn the base class into a smart pointer, but as outlined, often the class has to give itself, which you simply don't have a smart pointer to and all you've done is move the problem higher with the code here, if lostMemory went out of scope, (if the lamda had a different scope) and the lambda was called it would still have undefined behavior. The simplification of stackoverflow examples really doesn't do justice to how the code is actually used sadly. – Natio2 Feb 25 '23 at 07:36
  • std::function lambda; /*Scope change*/ {auto lostMemory = std::shared_ptr(new Foo()); lambda = lostMemory;} /*scope changed back*/ lambda(8); //Undefined behaviour! – Natio2 Feb 25 '23 at 07:40
  • Hmm no. the lambda would hold a smart pointer itself that would keep the object around. It is not UB. – Something Something Feb 25 '23 at 07:41
  • Oh do the captured variables create a copy? – Natio2 Feb 25 '23 at 07:42
  • 1
    In the example it has been captured by value. The lambda keeps a `std:;shared_ptr` as a member so the object is guarded by that shared_ptr as long as the lambda is in scope. – Something Something Feb 25 '23 at 07:46
  • Is there a way to make the lambda a weak pointer, so it doesn't stop the deletion of objects? – Natio2 Feb 25 '23 at 07:50
  • Isn't it exactly what you are doing with the raw pointer? – Something Something Feb 25 '23 at 07:53
  • nah like in my answer below, where the base object is bound by a shared_ptr, but the std::function uses a weak ptr, so it will do lambda things, until the object gets deleted, rather than the base object being bound to the lifetime of the lambda – Natio2 Feb 25 '23 at 07:54
  • but you have the same problem - the shared_ptr goes out of scope and the lambda does its UB thing. You mean, the lambda then "knows" that the object is gone? – Something Something Feb 25 '23 at 07:56
  • It's different because the lambda knows the base object is gone, and when you have a container of these, when you identify the base object is gone when calling the lambda, you can now remove the lambda cleaning up all the memory without tightly coupling the lambda to the object – Natio2 Feb 25 '23 at 08:00
  • With `std::weak_ptr` and `std:shared_ptr` that is exactly what you have. I updated the example. – Something Something Feb 25 '23 at 08:04
  • 1
    Yeah, that's more what I am aiming for. The only reason I did it my way is if the object itself has to give the lambda [this](int a){ ... }. But maybe I shouldn't be doing this. Anyway I appreicate your time, you've given me a new level of understanding around lambdas and a useful tool! (cppinsights.io) – Natio2 Feb 25 '23 at 08:09
  • 1
    I hope you know compiler explorer? www.godbolt.org. If you do, the link to cppinsights.io and quickbench are right on the top tab. – Something Something Feb 25 '23 at 08:10