Thread-safe cache using a linked list Are the final templated functions allocate and deallocate thread safe? The Cache object is a thread_local and that also makes FreeList a thread_local, the only parts that I'm still in doubt are the calls to std::malloc and std::free. Are they always thread safe or that depends on the implementation? Is it better to put a lock on them?

I think that the cached memory doesn't need locks since all the objects here are thread_local and each thread has a local copy of it. The only problem really are std::malloc and std::free, are they already synchronized?

Here's a self contained example of all of my code:

#include &lt;iostream&gt;

class FreeList {
    struct Node {
        Node * next;
    };

    Node * this_head;

public:
    static constexpr auto const NODE_SIZE = sizeof(Node);

    FreeList() noexcept
        : this_head() {
    }

    FreeList(FreeList &amp;&amp;) = delete;
    FreeList(FreeList const &amp;) = delete;

    FreeList &amp; operator =(FreeList &amp;&amp;) = delete;
    FreeList &amp; operator =(FreeList const &amp;) = delete;

    void push(void * const address) noexcept {
        auto const head = static_cast&lt;Node *&gt;(address);
        head-&gt;next = this_head;
        this_head = head;
    }

    auto pop() noexcept {
        void * const head = this_head;
        if (head) {
            this_head = this_head-&gt;next;
        }
        return head;
    }
};

template &lt;uint64_t const SIZE&gt;
class Cache {
    static_assert(SIZE &gt;= FreeList::NODE_SIZE);

    FreeList this_free_list;

public:
    Cache() noexcept
        : this_free_list() {
    }

    Cache(Cache &amp;&amp;) = delete;
    Cache(Cache const &amp;) = delete;

    ~Cache() noexcept {
        while (auto const address = this_free_list.pop()) {
            // Do I need a lock here?
            std::free(address);
        }
    }

    Cache &amp; operator =(Cache &amp;&amp;) = delete;
    Cache &amp; operator =(Cache const &amp;) = delete;

    auto allocate() {
        if (auto const address = this_free_list.pop()) {
            return address;
        }
        // Do I need a lock here?
        if (auto const address = std::malloc(SIZE)) {
            return address;
        }
        throw std::bad_alloc();
    }

    void deallocate(void * const address) noexcept {
        if (address) {
            this_free_list.push(address);
        }
    }
};

template &lt;typename TYPE&gt;
auto &amp; get() noexcept {
    thread_local TYPE local;
    return local;
}

template &lt;typename TYPE&gt;
auto &amp; get_cache() noexcept {
    return get&lt;Cache&lt;sizeof(TYPE)&gt;&gt;();
}

// Are these thread safe?

template &lt;typename TYPE&gt;
auto allocate() {
    auto const address = get_cache&lt;TYPE&gt;().allocate();
    return static_cast&lt;TYPE *&gt;(address);
}

template &lt;typename TYPE&gt;
void deallocate(void * const address) noexcept {
    get_cache&lt;TYPE&gt;().deallocate(address);
}

int main() {

    auto x = allocate&lt;int64_t&gt;();

    *x = 5;

    std::cout &lt;&lt; *x &lt;&lt; '\n';

    deallocate&lt;int64_t&gt;(x);

}
Thread safety
std::malloc and std::free are thread-safe by themselves... But Freelist and Cache aren't, unless exclusively accessed through allocate or deallocate.
In all other cases some synchronization is needed in FreeList::push and FreeList::pop (or alternatively, Cache::allocate and Cache::deallocate).

There are some options to make those two classes thread-safe for all access paths:

Move get, get_cache, Cache and FreeList into a class as private nested classes / member functions (so they aren't publicly accessible anymore) and make allocate and deallocate a friend of that class. (private/anonymous namespace won't work since that one has to ve just as accessible as allocate/deallocate because it has to reside in a header.)
Why move get and get_cache? Because references and pointers to thread_local objects can be shared with other threads - so a threads might get access to another threads thread_local Cache inside get.
thread_local objects are only thread-safe if no reference or pointer to them gets shared with other threads (which might happen accidentally, e.g. by a mismanaged lambda capture).
This means allocate and deallocate are not thread-safe as long as references to the underlying Cache (via get or get_cache) could be shared by someone else. And since allocate and deallocate have exactly the same level of access as get or get_cache the posted implementation is not thread-safe in the general case.

Add some locking inside FreeList, or make FreeList lock-free by making this_head a std::atomic&lt;Node*&gt;.
This would make the critical section inside Cache::allocate and Cache::deallocate thread-safe as well.



Memory management

Cache::allocate won't provide correctly aligned memory for alignments greater than std::max_align_t (std::malloc is only specified to support alignments to this value). This might cause problems if higher alignments are needed, e.g. for SSE or AVX instructions.
std::aligned_alloc could be used as a replacement. (Beware: std::aligned_alloc isn't available for MSVC yet.)

It isn't possible to create objects whose size is less than sizeof(void*) - so exchanging int64_t for int32_t inside main will fail to compile on 64-bit systems. This could be fixed by always allocating chunks of memory that are at least sizeof(void*) (or FreeList::NODE_SIZE, if you prefer).


Naming

Cache doesn't actually cache objects - only chunks of memory. FreeListAllocator might be a better name.

get is a very generic name. (get_)thread_local_instance or singleton might be more descriptive.

Similar, get_cache might better be get_allocator or get_thread_local_allocator.


General stuff

With some slight modifications Cache could be changed into a standard library compatible std::pmr::memory_resource - which would allow it to be used with standard containers.

If I had to design this, I'd likely make FreeList an adaptor over another allocator (taken as template parameter) who would be asked for memory if the list was empty. This design would allow easy composition of different allocation strategies. There's an excellent talk by Andrei Alexandrescu about this topic.