2

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;
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
John
  • 2,963
  • 11
  • 33
  • What makes you think it's still valid when it's gone out of scope? – Ted Lyngmo Dec 12 '20 at 02:19
  • 1
    It crashes in my windows. The address sanitizer throws `stack-use-after-scope` – Pablochaches Dec 12 '20 at 02:20
  • @TedLyngmo You see the code snippet works well. http://cpp.sh/6nnd4 – John Dec 12 '20 at 02:20
  • 4
    @John "works well" is one of the possible outcomes of undefined behavior. `*pFun` dereferences a pointer to an object that is no more ... – Ted Lyngmo Dec 12 '20 at 02:21
  • 2
    Just replace `std::function` with a simpler `int` and now ask yourself the same question. It's not like that a different type has a different lifetime. – Jack Dec 12 '20 at 02:28
  • @Jack2 Replacing `std::function` with a simpler `int`? How? `std::function`? I didn't quite get your idea. – John Dec 12 '20 at 02:33
  • The idea is for you to grasp the concept of lifetime. The object has gone out of scope. Derefencing a pointer to it makes your program have undefined behavior - even if it appears to work. ... and no, @Jack meant a plain `int`, a pointer to it, and then dereferencing that pointer after the `int` has gone out of scope. – Ted Lyngmo Dec 12 '20 at 02:34
  • @TedLyngmo I see. thank you. The key is lifetime of a variable. What confuses me is that the aforementioned code snippet works well when I posted such a question. Now I know that "works well" is one of the possible outcomes of undefined behavior.Anybody can provide an online link which such code fails, I want to do more tests? All the compilers on https://godbolt.org/z/cr5e3o go well. – John Dec 12 '20 at 02:38
  • @Pablochaches Is there an online link on which such code fails, I want to do more tests. All the compilers on https://godbolt.org/z/cr5e3o go well. – John Dec 12 '20 at 02:39
  • 2
    @John Compile it with the address sanitizer. It will show you it fails. Still, I dont understand why you are doing so much testing on this. It is undefined behaviour, so it will work some times, but it is never guaranteed. [Here is your link](https://godbolt.org/z/fv1qvK) – Pablochaches Dec 12 '20 at 02:42
  • Many people fight UB with the same thought. What makes UB even more confusing in C++ is that some compilers defines behavior (that goes above the C++ standard) for some situations. This is however not one I've heard about. This code has UB in all environments I've ever worked in. And do what @Pablochaches suggested. Use an address sanitizer - it's really revealing! – Ted Lyngmo Dec 12 '20 at 02:42
  • @Pablochaches Thank you for your patience and help. I fully understand it's an undefined behavior with your help now. I want to verify whether the object is destroyed or not after being pushed to `std::stl`(push object, not a pointer to the object). – John Dec 12 '20 at 02:48
  • @John I don't understand what "_being pushed to `std::stl` (push object, not a pointer to the object)_" means. You can have pointers all over the place. Just don't dereference those that points towards objects that are not "alive". Same-type objects may appear in the same space where the pointer points - and then it's ok to dereference again. Just keep track. – Ted Lyngmo Dec 12 '20 at 02:54
  • @TedLyngmo Sorry, English is not my mother language. I may mislead you. See this simple code. It clearly shows what I meant. https://godbolt.org/z/seoo6E. Push the object of `std::function` to `std::stl`, not push the pointer of `std::function`. **The object is still alive beyond the scope (i.e variable `fn1`) by this method?** – John Dec 12 '20 at 03:03
  • @John Ah, I _think_ I understand where your confusion comes from in this case. No, the object is not alive beyond the scope. There is a _copy_ of the original object still alive in the map. `fn1` is dead - the object in `mp` is not. – Ted Lyngmo Dec 12 '20 at 03:05
  • 1
    @TedLyngmo I see, thank you very much. I understand this matter at a different level with the help of you. – John Dec 12 '20 at 03:06
  • 1
    @John Cheers! Glad to help. – Ted Lyngmo Dec 12 '20 at 03:07

2 Answers2

4

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.

Jeff Spencer
  • 507
  • 2
  • 11
1

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.

Pablochaches
  • 968
  • 10
  • 25