C++ Logo

std-proposals

Advanced search

[std-proposals] Save Cascading Free of Smart Pointers

From: M.C.A. (Marco) Devillers <"M.C.A.>
Date: Tue, 16 Jul 2024 23:13:20 +0200
I came up against this and wanted to float an idea. It doesn't seem
popular but I think I am right. This should be resolved.

Document number: xxx
Date: 2024-7-16
Audience: GCC email list
Reply-to: marco.devillers_at_[hidden], std-proposals_at_[hidden]

I.
II. Introduction

Because C++ smart pointers are based on RAII it is easy to trigger an
overflow of the C stack since destructors call each other. Smart
pointers are supposed to be safe, smart pointers are likely to be used
extensively in the future, and this behaviour could make a large
number of C++ programs core dump unexpectedly.
This proposal is to remove this behaviour from GCCs standard library
and also showcases a small trick by which that can be done.

III. Motivation and Scope

We all want smart pointers since they allow for easy and safe memory
management, this desire is only expected to increase over the
following decades.

However due to RAII semantics it's easy to trigger an overflow of the
C stack once garbage goes out of scope. Observe the following trivial
program:

#include <iostream>
#include <memory>

struct list_node {
   using ptr = std::unique_ptr<list_node>;
   ~list_node() {
   }

   int x;
   ptr next;
};

int main() {
   list_node::ptr next = nullptr;

   for(int i = 0; i < 100000; ++i) { // decrease value to see it not segfault
       next = list_node::ptr(new list_node{i, std::move(next)});
   }
}

Cascading frees will make this program core dump depending the size of
the list. Please note, that that program will segfault on for current
data-driven days relatively tiny sizes.

I give it to you that this is unsafe and unwanted behaviour since now
every program employing nested structures can core dump easily and
unexpectedly. For example: GUIs, editors, parsers, transformers, etc.
The problem is only expected to worsen with more developers using safe
pointers.

The proposal is to remove this behaviour from the GCC standard library
by hardening smart pointers in the following manner: instead of
calling garbage recursively garbage is first offloaded to a stack and
that stack destroys objects until it is empty. Or by any other means
that removes segfaulting cascading frees.

A reference implementation (not concurrent) for unique pointers is
given as an Addendum. (The code is due to Alipha on libera.net).

IV. Impact On the Standard

This shouldn't impact other parts of the standard.

V. Design Decisions

Offload destructor calls to an explicit stack to make these calls
sequential instead of recursive. A non-concurrent implementation of
unique pointers is given below.

VI. Technical Specifications

This shouldn't change anything.

VII. Acknowledgements

This cascading free behaviour was noticed during the development of
the Egel language interpreter, and the author has great interest in
having this resolved. The problem was discussed on various channels
and together with Alipha on libera.net a solution was developed. The
idea was subsequently floated on the GCC mailing list where it was
forwarded.

VIII. Addendum

#include <iostream>
#include <functional>
#include <vector>
#include <utility>
#include <memory>

namespace detail {
  inline bool doing_cleanup = false;
  inline std::vector<std::function<void()>> ptr_cleanup;
}

template<typename T>
class safe_unique_ptr {
public:
  safe_unique_ptr() : ptr() {}
  safe_unique_ptr(T *p) : ptr(p) {}

  safe_unique_ptr(safe_unique_ptr &&other) :
ptr(std::exchange(other.ptr, nullptr)) {}

  safe_unique_ptr &operator=(safe_unique_ptr &&other) {
    cleanup(ptr);
    ptr = std::exchange(other.ptr, nullptr);
    return *this;
  }

  T &operator*() const { return *ptr; }
  T *operator->() const { return ptr; }

  ~safe_unique_ptr() { cleanup(ptr); }

private:
  void cleanup(T *p) {
    using namespace detail;

    if(!p) return;

    if(!doing_cleanup) {
      doing_cleanup = true;
      delete p;

      while(!ptr_cleanup.empty()) {
        std::function<void()> deleter = ptr_cleanup.back();
        ptr_cleanup.pop_back();
        deleted();
      }

      doing_cleanup = false;
    } else {
      ptr_cleanup.push_back([p]() { delete p; });
    }
  }

  T *ptr;
};

struct list_node {
  using ptr = safe_unique_ptr<list_node>;
  ~list_node() {}

  int x;
  ptr next;
};

Received on 2024-07-16 21:13:58