0

If the shared_ptr is destroyed, what happens to "this" if captured in a lambda to be run on a thread? Shouldn't it have thrown an exception in the below case since the object Test was destroyed before the thread could finish running.

#include <iostream>
#include <thread>
#include <chrono>

using namespace std;
using namespace std::this_thread; // sleep_for, sleep_until
using namespace std::chrono; // nanoseconds, system_clock, seconds


class Test 
{
    private:
    int testInt = 0;
    
    public:
    std::thread TestMethod()
    {
        auto functor = 
        [this]() ->void 
        {
            sleep_until(system_clock::now() + seconds(1));
            ++testInt; cout<<testInt<<endl;
        };
        
        std::thread t1(functor);
        testInt = 6;
        return t1;
    }
    
    ~Test()
    {
        cout<<"Destroyed\n";
        testInt = 2;
    }
};

int main()
{
    cout<<"Create Test\n";
    auto testPtr = std::make_shared<Test>();
    auto t = testPtr->TestMethod();
    testPtr = nullptr;
    cout<<"Destroy Test\n";
    t.join();

    return 0;
}

Output is

Create Test
Destroyed
Destroy Test
3

How is the lambda able to access testInt of a destroyed object ?

rstr1112
  • 308
  • 4
  • 13
  • 6
    Undefined behaviour is undefined. – Mike Vine Dec 09 '21 at 09:57
  • There are languages (like Java and Rust) that try to make undefined behavior impossible; C++ is not one of those languages. In C++, the programmer is expected to follow the rules, and if he doesn't, then whatever happens, happens, and any complaints about the resulting behavior are met with "fix your code so it doesn't break the rules". All in the name of maximum efficiency :) – Jeremy Friesner Dec 09 '21 at 15:23

1 Answers1

1

In practice, The hardware that executes your program knows nothing about objects or member variables or references or pointers. What was a variable in your C++ source code becomes a virtual memory location in the executable, and what was a reference or a pointer becomes a virtual memory address. When an object is "destroyed" by your program, that means nothing to the machine. The program just stops using that part of virtual memory for that purpose, and if the program continues to run, it quite likely will re-allocate that memory for some other purpose soon after.

If your program keeps a dangling reference/pointer to an object that previously was "destroyed," then any access through that pointer/reference will be an access to a memory location that either now is unused, or now is part of some entirely different object from the one that was there before. This typically leads to incorrect behavior of the program (a.k.a., "bugs") that can be especially difficult to diagnose.

Shouldn't it have thrown an exception in the below case since the object Test was destroyed?

Requiring every program to detect the use of a dangling reference or pointer would have a huge, detrimental impact on the performance of most programs. To fetch the value of some variable through a reference, on most CPU architectures, might be a single machine instruction. Detecting whether the reference was valid could require the program to execute tens or hundreds of instructions.

The language standard deems use of a dangling reference or pointer to be undefined behavior. That's their way of saying, you shouldn't do it, but it's your responsibility to ensure that your program doesn't do it.

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57
  • One may ask how to better organize this code to avoid making the mistake. When you captured "this" in a lambda, you implicitly shared ownership of the object without following the discipline of std::shared_ptr. In a different design, the lambda (not part of a member function) could capture a copy of the shared pointer rather than capturing "this". Then the object would live as long as the lambda. – WilliamClements Dec 13 '21 at 17:26
  • @WilliamClements, The author of some arbitrary class might have no control over how instances of the class are allocated. (Almost certainly true if the class is part of some library API.) Probably the best way to "avoid making the mistake" would be to use the [PIMPL design pattern](https://en.cppreference.com/w/cpp/language/pimpl), in which the publicly visible class is nothing but a wrapper for a `shared_ptr` to a _heap-allocated_ instance of some hidden, private class. – Solomon Slow Dec 13 '21 at 17:45
  • When using PIMPL, Members of the public class can be freely copied, and all copies will refer to the same private instance, which will be automatically deleted when the last copy is destroyed. – Solomon Slow Dec 13 '21 at 17:50