Search code examples
c++cmalloclanguage-lawyerfree

Can I free() something in C that has been malloc()'ed in C++?


I'm writing a wrapper for a library written in C++, such that it can be used from C. In the wrapper code I'm making lots of copies of the underlying data of c++ containers. E.g. if the c++ library function returns a std::vector<int>, my wrapper will return a struct of the form {size_t len; size_t size; void *arr;}, where arr contains a copy of the data from the vector. When the user is done with the data, they must free it.

My question is: is it always legal for the user (C-code) to call free() on pointers which have been malloc():d in C++? Or must I create an equivalent function in my wrapper code?


Solution

  • You can mix C++'s std::malloc and C's free

    std::malloc in C++ is defined in <cstdlib> which is said to have the same contents and meaning as <stdlib.h> in C (with some variations, such as namespacing). Furthermore, [c.malloc] says:

    void* aligned_alloc(size_t alignment, size_t size);
    void* calloc(size_t nmemb, size_t size);
    void* malloc(size_t size);
    void* realloc(void* ptr, size_t size);
    

    Effects: These functions have the semantics specified in the C standard library.

    This means that you can allocate some memory with std::malloc in C++ an pass it to some C function which calls free.


    Note: mixing different standard libraries or mixing different builds (debug/release) of the same standard library might still be an issue, but that applies to all language features.

    The C++ standard library doesn't use std::malloc

    That being said, it would not be safe to use free for memory allocated by something like a std::vector like you've suggested. All containers which do some memory allocation use std::allocator by default, which uses operator new.

    Mixing new and free would be undefined behavior, even if the underlying OS functions to obtain and release memory are the same.

    How to use std::vector in C

    // C23
    struct vector {
        // note: 3 pointers in size is usually the bare minimum which is needed for
        //       a std::vector.
        alignas(void*) unsigned char data[3 * sizeof(void*)];
    };
    
    // Note the symmetric interface; it doesn't matter how init/destroy are
    // implemented to the user.
    void vector_init(struct vector*);
    void vector_destroy(struct vector*);
    // Also add this and other functions to make the vector useful.
    void vector_push(struct vector*, int element);
    
    int main() {
        vector v;
        vector_init(&v); // no malloc, no free
        vector_push(&v, 42);
        vector_destroy(&v);
    }
    

    So far, we've basically just defined a struct vector to contain some amount of bytes, and three opaque functions. All the code is C23-compatible, and we could implement the actual functionality in C++.

    // C++20
    static_assert(alignof(vector::data) >= alignof(std::vector));
    static_assert(sizeof(vector::data) >= sizeof(std::vector));
    
    extern "C" void vector_init(vector* v) {
        std::construct_at(reinterpret_cast<std::vector<int>*>(v->data));
    }
    
    extern "C" void vector_destroy(vector* v) {
        std::destroy_at(reinterpret_cast<std::vector<int>*>(v->data));
    }
    
    extern "C" void vector_push(vector* v, int element) {
        auto* vec = std::launder(reinterpret_cast<std::vector<int>*>(v->data));
        vec->push_back(element);
    }
    

    The C++ side uses std::construct_at (or prior to C++20, you could use placement new). We create a std::vector in the raw bytes of vector::data. Note that we aren't calling new, delete, malloc, or free anywhere in this code. std::vector is still responsible for all the memory management.