When the object of std::function is destroyed?

Viewed 265

When the object of std::function is destroyed? Why the pointer to the object of std::function is still valid when the variable fn1 is out of the scope(you see the code snippet works well, http://cpp.sh/6nnd4)?

// function example
#include <iostream>     // std::cout
#include <functional>   // std::function, std::negate

// a function:
int half(int x) {return x/2;}

int main () {
    std::function<int(int)> *pFun;
    
    {
        std::function<int(int)> fn1 = half;                    // function
     
         pFun= &fn1;   
         std::cout << "fn1(60): " << (*pFun)(60) << '\n';
    }

    std::cout << "fn1(60): " << (*pFun)(90) << '\n';

  return 0;
}
2 Answers

The short answer is that C++ doesn't validate the contents of a pointer. That's the developer's responsibility. It only validates that the variable pFun is in scope.

In C++ it is often the developers responsibility to make sure that their pointers are pointing to valid objects. In this case, fn1 has likely been created on the stack. Since many compilers implement local variables that way. When fn1goes out of scope the compiler will no longer allow the use of the variable fn1. However, what happens to the memory on the stack backing fn1 is not defined. In your case, the compiler left it untouched so (*pFun)(90) happens to still work. However, running the same code on another compiler may not. In fact, simply turning on compiler optimization may cause it to stop working. It all depends on whether the compiler re-uses that memory, or frees it from the stack.

In your example the scope is a code block. If the scope had instead been a separate function named MyFunction2then when MyFunction2exited the stack memory associated with fn1 would have been freed off the stack and the memory available for reuse. Both by interrupts and whatever code was called after MyFunction2. So the data would be more likely to have been changed such that (*pFun)(90) faulted.

Now debugging something like this crashing is fairly strait forward. Imagine if you had written to pFun instead, this would have caused memory corruption of what ever object happened to be using that memory after fn1 had gone out of scope.

Why the pointer to the object of std::function is still valid when the variable fn1 is out of the scope?

Let me present a simpler example, using ints. But if you are brave, you can try to read the assembler for the std::function version.

int main () {
    int a = 0;
    int *c = nullptr;
    {
        int b = 1;
        c = &b;
    }
    a = *c;
    return a;
}

This is generated with gcc 10.2 -O0, but the other compilers have a really similar output. I will comment it to aid the understanding.

main:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], 0    # space for `a`
        mov     QWORD PTR [rbp-16], 0   # space for `c`
        mov     DWORD PTR [rbp-20], 1   # space for `b`
        lea     rax, [rbp-20]           # int *tmp = &b  Not really, but enough for us
        mov     QWORD PTR [rbp-16], rax # c = tmp
        mov     rax, QWORD PTR [rbp-16] 
        mov     eax, DWORD PTR [rax]    # int tmp2 = *tmp
        mov     DWORD PTR [rbp-4], eax  # a = tmp2
        mov     eax, DWORD PTR [rbp-4]  
        pop     rbp                         
        ret                             # return a

And the program return 1, as expected when you see the assembler. You can see, b was not "invalidated", because we did not roll the stack back, and we didnt change its value. This will be a case like the one you are in, were it works.

Now lets enable optimizations. Here is it compiled with -O1, the lowest level.

Here is it compiled with gcc:

main:
        mov     eax, 0
        ret

And here is it compiled with clang:

main:                                   # @main
        mov     eax, 1
        ret

And here is with visual studio:

main    PROC                                            ; COMDAT
        mov     eax, 1
        ret     0
main    ENDP

You can see the results are diferent. It is undefined behaviour, each implementation will do as it sees fit.

Now this is happening with some local variables in the same function. But consider this cases:

  1. It was a pointer, lets say allocated with new. You already called delete. What guaranties that that memory is not used by someone else now?

  2. When the stack grows, the value will eventually be overiden. Consider this code:

int* foo() {
    int a = 0;
    return &a;
}

void bar() {
    int b = 1;
}


int main () {
    int *ptr = foo();
    bar();
    int a = *ptr;
    return a;
}

It didnt return 1 or 0. It returned 139.

And here is a good read on this.

Related