0

Found similar questions: Is it safe to `delete this`? I know that it's unsafe to access member variable. Because this becomes a dangling pointer after delete. But what about stack variable?Clang ASAN does report error if member variable is accessed. But it does not report any problems about stack variable access.

IMHO. Stack is destroyed after the current execution thread is finished. So it is probably safe to access stack variable even if this is deleted. Is there any better solution in this scenario? Following is the test case.

#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <ratio>
#include <string>
#include <system_error>
#include <thread>
#include <vector>

std::mutex g_iostream_mutex;

class TestDeleteBase {
  int task_num_ = 0;
  std::atomic<int> counter_;

 public:
  virtual void Run() = 0;
  void Init(int task_num) {
    task_num_ = task_num;
    counter_.store(0, std::memory_order_relaxed);
  }
  void RunParallel() {
    int local = counter_;
    if (counter_.fetch_add(1, std::memory_order_acq_rel) == task_num_ - 1) {
      delete this;
      {
        std::lock_guard<std::mutex> guard(g_iostream_mutex);
        std::cout << std::this_thread::get_id() << " deleted this\n";
      }
      return;
    } else {
      {
        std::lock_guard<std::mutex> guard(g_iostream_mutex);
        std::cout << std::this_thread::get_id() << " not delete \n";
      }
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    {
      std::lock_guard<std::mutex> guard(g_iostream_mutex);
      std::cout << std::this_thread::get_id() << " Access " << local << '\n';
      std::cout << std::this_thread::get_id() << " Still alive\n";
    }
  }
  virtual ~TestDeleteBase() {}
};

class TestDelete : public TestDeleteBase {
  void Run() override {}
};

int main(int argc, char* argv[]) {
  TestDeleteBase* obj = new TestDelete();
  obj->Init(5);
  std::vector<std::thread> threads;
  for (int i = 0; i < 5; ++i) {
    threads.emplace_back(&TestDeleteBase::RunParallel, obj);
  }
  for (auto&& thread : threads) {
    thread.join();
  }
}
8.8.8.8
  • 678
  • 7
  • 11
  • Should be safe since it's not a member of the instance, but I can't find where you're accessing the automatically allocated variable. The best candidate seems to be `local`, but it's not used between `delete this` and the next `return`. – user4581301 Jul 15 '22 at 03:43
  • `TestDeleteBase t; t.Init(1); t.RunParallel();` **BOOM** The constructor should be protected and private in derived/final classes or protected with a `private: struct Priv {};`. `TestDeleteBase* obj = new TestDelete(); t->RunParallel();` *BOOM* Why is `Init` not the constructor? Or at least initialize the class to sensible values. – Goswin von Brederlow Jul 15 '22 at 06:27
  • @user4581301 You are right. The stack variable is `local`. And it's accessed by other threads after `this` is deleted – 8.8.8.8 Jul 15 '22 at 06:55
  • @8.8.8.8 How would they access it? You're not storing any reference or pointer to it anywhere. – molbdnilo Jul 15 '22 at 07:03

1 Answers1

2

The problem is having counter_ equal to task_num_ - 1 does not mean all threads are finished. It just mean that the fetch_add call has been executed by all threads here. The thing is the == in the expression counter_.fetch_add(1, std::memory_order_acq_rel) == task_num_ - 1 is parsed from left to right. Thus, the fetch_add must be executed before task_num_ - 1 so the threads must access to the task_num_ member attribute after the fetch_add (especially because of the memory_order_acq_rel memory ordering). The point is the attribute can be deleted because the last thread can delete the operation meanwhile (which is very unlikely but still possible).

Besides, local = counter_ do not guarantee to get the last value so the printing will likely be incorrect. You can extract the correct value by storing the result of fetch_add in local.

Object self destruction if generally a very bad idea because it is very bug prone as said in the question link. Even when you succeed to make the code work, the code is not easy to maintain: another developer might not consider this issue when modifying the code (or even you several month/years after). The best thing to do is to delay delete and the general solution to self destruction is to define an entity responsible for the deletion of the object. In this case, the main function can be responsible for that because it is the one doing the thread.join. All threads are guaranteed to be freed after the loop by design.

Jérôme Richard
  • 41,678
  • 6
  • 29
  • 59
  • Very enlightening on the order of delete operation. Except correctness, Is it safe to access `local` in this example after delete operation has been executed? – 8.8.8.8 Jul 15 '22 at 10:29
  • 1
    AFAIK, yes, this is safe, because `local` is not an attribute and stored on the stack left unaffected by the `delete this`. Btw, you should be careful about virtual calls because they can access to the vtable of the class referenced by the object so calling a virtual function is not safe after the `delete this` (this looks fine here though). – Jérôme Richard Jul 15 '22 at 10:45
  • Thanks for pointing out virtual function. It's unsafe to call virtual functions after `delete this`. Because `vptr` may not point to the correct `vtable` after object deconstruction. – 8.8.8.8 Jul 17 '22 at 03:07