On Tue, 16 Jul 2024, 18:51 M.C.A. (Marco) Devillers via Gcc, < gcc@gcc.gnu.org> wrote:
> 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 > Where does it exist in the library? The problem in your program is easily avoided, without changing the standard or GCC's implementation of the 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. > So don't do that then. It's easy to add a loop to clean up the list. Or create an actual list class to manage the nodes and do that loop in its destructor. Nobody is forced to define a list only in terms of nodes, rather than a list class that manages the nodes. > 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; > }; >