Home > database >  Accessing data on heap in c
Accessing data on heap in c

Time:05-19

I have been trying to dive deeper into the limitations of pointers to see how they effect the program behind the scenes. One thing my research has led me to is the variables created by pointers must be deleted in a language like C , otherwise the data will still be on memory.

My question pertains to accessing the data after a functions lifecycle ends. If I create a pointer variable within a function, and then the function comes to a proper close, how would the data be accessed? Would it actually be just garbage taking up space, or is there supposed to be a way to still reference it without having stored the address in another variable?

CodePudding user response:

There's no automatic garbage collection. If you lose the handle (pointer, reference, index, ...) to your resource, your resource will live ad vitam æternam.

If you want your resources to cease to live when their handle goes out of scope, RAII and smart pointers are the tool you need.

If you want your resources to continue to live after their handle goes out of scope, you need to copy the handle and pass it around.

CodePudding user response:

Using standard smart pointers std::unique_ptr and std::shared_ptr memory is freed when pointer goes out of scope. After scope ends object is immediately destroyed freed and there is no way to access it anymore. Unless you move/copy pointer out of scope to bigger scope, where it will be deleted.

But there is not so difficult to implement lazy garbage collector. Same as before you use smart pointers everywhere, but lazey variant. Now when pointer goes out of scope its object is not immediately destroyed freed, but instead is delegated to lazy garbage collector, which will destroy free it later in a separate thread. Exactly this lazy behaviour I implemented in my code below.

I implemented following code from scratch just for fun and as a demo for you, there is no big point why not to use standard greedy freeing techniques of std::unique_ptr and std::shared_ptr. Although there is one very important use case - std::shared_ptr constructs objects at well known points in code, when you call constructor, and you know construction time well, but destroys objects at different undefined points in code and time, because there are shared copies of shared pointer. Thus you may have long destruction delays at unpredicted points in time, which may harm realtime high performance code. Also destruction time might be too big. Lazy deleting moves destruction into separate thread where it can be deleted at its own pace.

Although smart pointer is lazily disposed at scope end, but yet for some nano seconds (or even micro seconds) you may still have access to its undestroyed/unfreed memory, of course time is not guaranteed. This just means that real destruction can happen much later than scope ends, thus is the name of lazy garbage collector. You can even tweak this kind of lazy garbage collector so that it really deletes objects lets say after 1 milli second after theirs smart pointers have been destroyed.

Real garbage collectors are doing similar thing, they free objects much later in time and usually do it automatically by finding bytes in memory that look like real pointers of heap.

There is a Test() function in my code that shows how my lazy variants of standard pointers are used. Also when code is runned you may see in console output that it shows something like:

Construct Obj(  592)
Construct Obj( 1264)
LazyDeleter Dispose( 1264)
LazyDeleter Dispose(  592)
Test finished
Destroy ~Obj( 1264)
Destroy ~Obj(  592)

Here in parenthesis it shows id of object (lower bits of its pointer). You may see that disposal and destruction is done in order exactly opposite to construction order. Disposal to lazy garbage collector happens before test finishes. While real destruction happens later in a separate thread after test finishes.

Try it online!

#include <deque>
#include <atomic>
#include <mutex>
#include <thread>
#include <array>
#include <memory>
#include <iostream>
#include <iomanip>


using DelObj = void (void *);
void Dispose(void * obj, DelObj * del);

template <typename T>
struct LazyDeleter {
    void operator ()(T * ptr) const {
        struct SDel { static void Del(void * ptr) { delete (T*)ptr; } };
        std::cout << "LazyDeleter Dispose(" << std::setw(5) << uintptr_t(ptr) % (1 << 16) << ")" << std::endl;
        Dispose(ptr, &SDel::Del);
    }
};

template <typename T>
using lazy_unique_ptr = std::unique_ptr<T, LazyDeleter<T>>;

template <typename T>
std::shared_ptr<T> make_lazy_shared(T * ptr) {
    return std::shared_ptr<T>(ptr, LazyDeleter<T>{});
}

void Dispose(void * obj, DelObj * del) {
    class AtomicMutex {
    public:
        auto Locker() { return std::lock_guard<AtomicMutex>(*this); }
        void lock() { while (f_.test_and_set(std::memory_order_acquire)) {} }
        void unlock() { f_.clear(std::memory_order_release); }
        auto & Flag() { return f_; }
    private:
        std::atomic_flag f_ = ATOMIC_FLAG_INIT;
    };

    class DisposeThread {
        struct Entry {
            void * obj = nullptr;
            DelObj * del = nullptr;
        };
    public:
        DisposeThread() : thr_([&]{
            size_t constexpr block = 32;
            while (!finish_.load(std::memory_order_relaxed)) {
                while (true) {
                    std::array<Entry, block> cent{};
                    size_t cent_cnt = 0;
                    {
                        auto lock = mux_.Locker();
                        if (entries_.empty())
                            break;
                        cent_cnt = std::min(block, entries_.size());
                        std::move(entries_.begin(), entries_.begin()   cent_cnt, cent.data());
                        entries_.erase(entries_.begin(), entries_.begin()   cent_cnt);
                    }
                    for (size_t i = 0; i < cent_cnt;   i) {
                        auto & entry = cent[i];
                        try { (*entry.del)(entry.obj); } catch (...) {}
                    }
                }
            }
        }) {}
        ~DisposeThread() {
            while (!entries_.empty())
                std::this_thread::yield();
            finish_.store(true, std::memory_order_relaxed);
            thr_.join();
        }
        void Add(void * obj, DelObj * del) {
            auto lock = mux_.Locker();
            entries_.emplace_back(Entry{obj, del});
        }
    private:
        AtomicMutex mux_{};
        std::thread thr_{};
        std::deque<Entry> entries_;
        std::atomic<bool> finish_ = false;
    };

    static DisposeThread dt{};
    dt.Add(obj, del);
}

void Test() {
    struct Obj {
        Obj() { std::cout << "Construct Obj(" << std::setw(5) << uintptr_t(this) % (1 << 16) << ")" << std::endl << std::flush; }
        ~Obj() { std::cout << "Destroy ~Obj(" << std::setw(5) << uintptr_t(this) % (1 << 16) << ")" << std::endl << std::flush; }
    };
    {
        lazy_unique_ptr<Obj> uptr(new Obj());
        std::shared_ptr<Obj> sptr = make_lazy_shared(new Obj());
        auto sptr2 = sptr;
    }
    std::cout << "Test finished" << std::endl;
}

int main() {
    Test();    
}
  • Related