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?
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.
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.
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.