Document number: SCF4C++00 Date: 2024-7-16 Audience: GCC email list Reply-to: marco.devill...@gmail.com, gcc@gcc.gnu.org
I. 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. II. 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. 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; };