GC in C ++: Overcoming the Temptation

    Recently, a note appeared about a simple and efficient “manual” garbage collection in C ++. I fully admit that local garbage collection inside some complex class (where active low-level work with pointers is going on) can be justified. But at the scale of a large program, there is a more reliable and simple method to get rid of memory leaks. Without claiming to be a “method for all occasions,” I really hope that it will make life easier for at least some of the readers.

    The essence of the method is extremely simple: if each object is a variable of some scope or a simple ("stack") member of another object, then even when the program crashes from an unhandled exception, the correct cleaning will always occur. The task is to reduce the whole variety of dynamic scenarios to this scheme.

    1. Each object has exactly one owner.
    The most important principle. First of all, it means that during the execution of a program an object can be deleted only by a single owner object, and by no one else.
    In the simplest, “static” case, this simply means including the object in the owner class in the usual way as a member. I contrast it with more exotic options for including an object in the class of its owner through a pointer or link (note, not in any class, but in the owner class).
    Root program objects are declared as stack variables in main (). Moreover, it is still better in main () than in the form of global variables, because in the first case it is possible to guarantee the cleaning order (I contrast the case with a set of global objects scattered by translation units).

    By placing all objects in this way, even after throwing an unhandled exception, the correct stripping will be performed.
    class SomeParent
    {
        Child1 child1;
        Child2 child2;
    };
    class Root
    {
    public:
        void Run();
    private:
        SomeParent entry;
    };
    int main(int argc, char **argv, char **envp)
    {
        Root().Run(); //даже при выбросе исключения, не будет утечек
    }
    
    Of course, this is the most obvious case, not involving any dynamics.
    It is more interesting when an object needs to be created in the course of execution:

    2. Owning containers.
    To store a dynamically created object, use a container with auto-cleaning. The container itself is declared an ordinary, "stacked" member of the class. This may be some kind of smart pointer option, or your own container implementation:
    template  class One
    {
    public:
        One(); //изначально пустой
        One(T *); //владение объектом переходит контейнеру
        void Clear(); //уничтожение объекта вручную
        T *operator->(); //доступ к указателю
        T *GetPtr();
        ~One(); //автозачистка
    };
    //--- использование:
    class Owner
    {
        One dynamicEntity;
    };
    
    In this case, we can say that the container is the owner of the object.

    3. Owning arrays.
    They are used in the case when you need to operate with a collection of objects, united by any criterion. The peculiarity of such an array is understandable: in the destructor, it correctly destroys all its elements. When added to the array by pointer, the object becomes the property of the array and is also destroyed by it in the destructor.
    template  class Array
    {
    public:
        T & Add();
        T & Add(const T &); //копирование
        T & Add(T *); //владение переходит массиву
        ~Array(); //уничтожает входящие элементы
    };
    //для ассоциативных массивов - аналогично:
    template  class ArrayMap
    {
    public:
        T & Add(const K &);
        T & Add(const K &, const T &); //копирование
        T & Add(const K &, T *); //владение переходит массиву
        ~ArrayMap(); //уничтожает входящие элементы
    };
    //--- использование:
    class Owner
    {
        Array        stringsArray;
        ArrayMap anotherStringsCollection;
    };
    
    It is clear that the owner of the entire collection of objects is an array, which is an ordinary, "stacked" member of the owner class.
    From such owning primitives, you can create fairly complex models. The next level of difficulty is the transfer of objects between owners:

    4. Transfer of objects transfers ownership.
    Each object has exactly one owner. The owner can transfer the object to another owner, but he loses access to the object.
    You can transfer the object along with ownership by adding destructive copying of the internal pointer to arrays and containers:
    template  class One
    {
    public:
        //...
        One(const One &source); //ptr = source.ptr; source.ptr = NULL;
        void operator=(const One &source); //ptr = source.ptr; source.ptr = NULL;
        bool IsEmpty(); //узнать, владеем ли мы объектом
    private:
        mutable T *ptr;
    };
    //аналогичный функционал добавляется и для массивов
    

    As a result, if the owner returned an array or container from his member function, then he actually transferred the ownership of the child objects to the calling object. The caller has become the new owner. And objects have no chance of becoming memory leaks, since they are guaranteed to be cleaned by someone.

    I remind you again that this all works only if we strictly adhere to the rule that any object has exactly one owner.
    And this means that even if the owner passes the link or pointer to the object “outside”, the recipient can ask this object to participate in some kind of functionality (by calling open member functions of the object). But this object cannot be deleted, since it is not its owner:
    class CleverEntity
    {
    public:
        void UpdateUI(Window *window)
        //получая указатель, получатель соглашается на использование объекта,
        //но не будет влиять на его жизненный цикл
        {
            //window->...
            //запрещено: delete window и прочие попытки уничтожить
           //            либо перехватить владение объектом
        }
    };
    class WindowWorker
    {
    public:
        void UpdateUI()
        {
            entity.UpdateUI(window.GetPtr());
        }
    private:
        CleverEntity  entity;
        One   window;
    };
    


    That's all.
    Maybe not a "silver bullet", but for the vast majority of applications, it is quite enough.
    More complex scenarios, if desired, can be made out in the comments.

    PS To those interested in this topic, I recommend that you familiarize yourself with the library where all these concepts are already implemented - the Core package ( concept , example array ) of the U ++ framework (BSD license). It explains this technique in its own way, as well as some other interesting features ( fast compilation , fast destructive copying , acceleration of arrays by an order of magnitude ).

    Some theoretical aspects of the approach were outlined in one from previous articles.

    Also popular now: