Can I save callback functions that expect referenced variable arguments?

A few day ago I asked how to save a callback function in a class. Now, I don't know how to save a callback function with referenced arguments. I'll give you an example.

By the way, I can't just put & signs on gameObjects like std::vector<GameObject>&gameObjects because my program will crash. I'm definitely missing something.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  // main.cpp * Note: I'd like to receive &references to gameObjects
  int Update(std::vector<GameObject> gameObjects, int a) {
     
  }

  int main () {
     // ...
     game.InitCallback(Update);
  }
  
  // game.h
  public: 
    void InitCallback(int(*fn)(std::vector<GameObject>,int)) {
        callback = fn;
    }    

    void Update(std::vector<GameObject> gameObjects, int a) {
        callback(gameObjects, a);
    }
  private:
     int(*callback)(std::vector<GameObject>, int);

  // engine.cpp
  void Engine::Update() {
     game.Update(gameObjects, 2);
  }

Converting the code you've posted to using references should pose no problem. It could only be a problem if the callback does anything strange with the gameObjects parameter, such as saving a pointer to it or one of its elements in a variable that outlives the call. E.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std::vector<GameObject> *go;

int Update(std::vector<GameObject> &gameObjects, int a) {
    //Escaping reference!
    go = &gameObjects;
}

int main(){
    //...

    game.InitCallback(Update);

    //(An update happens.)

    //Is this valid or not? Impossible to know from the code you've shown.
    go->clear();
}
Last edited on
Thanks Helios. I think I'll just try callbacks with <functional>.
Be careful. The lifetime of objects within an std::function is arbitrary.
> The lifetime of objects within an std::function is arbitrary.

No. An object of type std::function<T> stores a copy of the target callable object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <functional>

int main()
{
    std::function< int(int) > fn ;

    {
        int local_a = 78 ;

        // the closure (local_b) stores a copy of local_a (captured by value)
        const auto local_b = [local_a] ( int arg ) { std::cout << "closure\n" ; return arg + local_a ; } ;

        fn = local_b ; // fn stores a copy of local_b
        
        local_a = -10000 ;
    }

    auto result = fn(159) ; // call is forwarded to the copy of the closure
    std::cout << "result: " << result << "\n--------------\n" ; // 159 + 78

    {
        struct callable
        {
            callable() = default ;
            callable( const callable& ) { std::cout << "callable::copy constructor\n" ; }
            callable( callable&& ) = default ;
            ~callable() { std::cout << "callable::destructor\n" ; }

            int operator() ( int arg ) const { std::cout << "callable::operator()\n" ; return arg + value ; } ;
            int value = 12345 ;
        };

        callable c ;
        fn = c ; // copy c, then move the copy into fn (equivalent to std::function< int(int) >{c}.swap(fn) )
        c.value = -10000 ; // this does not affect the copy in fn
        
        // destructor of the (temporary) moved-from callable object
        // destructor of c
    }
    std::cout << "--------------\n" ;

    result = fn(159) ; // call is forwarded to the copy of the callable object
    std::cout << "result: " << result << '\n' ; // 159 + 12345

    // destructor of the copy of callable stored in fn
}

http://coliru.stacked-crooked.com/a/05b2a243bbc63b6a
An object of type std::function<T> stores a copy of the target callable object.
Of course, but you can run into problems if you bind by reference, or if your objects contain pointers to objects with automatic lifetimes, or stuff like that. Once you have a closure that may escape the scope where it was declared, it may become more difficult to figure out what needs to be done to maintain its validity throughout its lifetime.
OP has already said he's getting crashes with what seems perfectly reasonable code, so we don't know what he might be doing. Switching to std::function willy-nilly might only make things more complicated, not to mention more difficult to debug.
> you can run into problems if...

Those problems have nothing specific to do with std::function<>. Those problems (undefined behaviour) would be there whether or not the callable object was wrapped in a call wrapper.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
auto foo()
{
    int i = 7 ;
    return [&i]() { ++i ; } ;
}

auto bar()
{
    int i = 7 ;
    return std::bind( []( int a ) { ++a ; }, std::ref(i) ) ;
}

auto baz( int arg = 0 )
{
    static int& r = arg ;
    ++r ;
}

int main()
{
    foo()() ; // undefined behaviour
    bar()() ; // undefined behaviour
    /* bar() */ baz() ;  // EDIT
    /* bar() */ baz() ; // undefined behaviour
}
Last edited on
That's an unnecessary nitpick, and it doesn't invalidate the warning I gave.
> it doesn't invalidate the warning I gave.

The warning you gave was: "The lifetime of objects within an std::function is arbitrary."

Which is completely invalid.
An object of type std::function<T> stores a copy of the target callable object;
the life-time of that object not arbitrary, it has a well defined life-time.

This has undefined behaviour; but that is not because the the lifetime of objects within a std::vector<> is arbitrary.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...

int* foo()
{
    int temp = 8 ;
    return std::addressof(temp) ;
}

int main()
{
    std::vector< int* > seq ;
    seq.push_back( foo() ) ;

    if( seq.front() ) std::cout << *seq.front() << '\n' ; // undefined behaviour
}
"Arbitrary" in the sense that it may outlast the scope that created it for arbitrarily long, not in the sense that the lifetime is some uncertain quantity.

I would argue that yes, your example has undefined behavior precisely because a pointer's lifetime may be arbitrarily longer than the thing it points to. Any object that may contain pointers -- such as std::vector<T *>, std::list<T *>, std::tuple<T *>, lambdas that bind pointers, etc. -- is capable of producing escaping references.
Topic archived. No new replies allowed.