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 freestd::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::mallocThat 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With