Reputation: 87220
I've seen many variants of this question asked here, but I still feel my specific case is different.
My goal is wrapping a C API that looks like this:
TF_Buffer* buf = TF_AllocateBuffer();
// ...
TF_DeleteBuffer(buf);
Since I have many of these objects, I'd love to create a generic type named handle
that could hold a given pointer and call the appropriate deallocator upon destruction. My imagined use case would be
class buffer : public handle<TF_Buffer, TF_DeleteBuffer> {
public:
buffer(TF_Buffer* b): handle(b) {}
}
unfortunately I'm unable to get this to work since TF_DeleteBuffer
is a simple function (of type void TF_DeleteBuffer(TF_Buffer*)
). I did manage to work around the issue with creating a function object for the function, so the following does work
template<typename Obj, typename Deleter>
class handle {
public:
Obj* obj;
handle(Obj* o): obj(o) {};
~handle() { if (obj) Deleter()(obj); }
};
struct buffer_deleter {
void operator()(TF_Buffer* b) { TF_DeleteBuffer(b); }
};
class buffer : public handle<TF_Buffer, buffer_deleter> {
public:
buffer(TF_Buffer* b): handle(b) {}
}
but it feels dirty having to define the buffer_deleter
class just for this purpose. I'd imagine something like this ought to work (with or without the std::function
)
template<typename Obj, std::function<void(Obj*)> Deleter>
class handle {
// ...
}
but I can't find a way to make the compiler happy. From what I understand, this is somewhat similar to std::unique_ptr
which accepts a deleter type object, vs std::shared_ptr
which accepts a deleter function pointer and stores it in the shared object. I don't mind storing the pointer explicitly (and using extra memory), but at the same time, given I'll be creating lots of these types, I'd like to have some way of making it syntactically nice. I really do not want to pass the deleter pointer to each instance of the object being created, which is why I'm trying to hide it in the template.
Upvotes: 5
Views: 620
Reputation: 2158
C++ does not care much about types of functions. For instance, check this:
#include<iostream>
using namespace std;
int func(char* str) { cout << str << endl; return strlen(str); }
template<class T> T executor(T f) { return f; }
int main()
{
int x = executor(func)("hello");
return 0;
}
The only thing it cares, when it comes to using the real types, they must satisfy the operation performed inside the class. It is perfectly OK to do in Visual C++ something like this:
#include<iostream>
using namespace std;
void strdtor(char* str)
{
cout << "deleting " << str << endl;
delete[] str;
}
template <class T, typename TDeletor> class memorizer
{
TDeletor& dtor;
T buffer;
public:
memorizer(T buf, TDeletor dt) : buffer(buf), dtor(dt){}
~memorizer(){dtor(buffer);}
};
int main()
{
char* c = new char[10];
sprintf_s(c, 10, "hello");
memorizer<char*, void(char*)> m(c, strdtor);
return 0;
}
As lambda:
char* d = new char[10];
sprintf_s(d, 10, "world");
memorizer<char*, void(char*)> m2(
d,
[](char* x) -> void
{
cout << "lambla deleting " << x << endl;
delete[] x;
});
Upvotes: 0
Reputation: 40080
I'd reuse std::shared_ptr
. It just works in all cases and have been thoroughly tested:
template<class Buffer, class Destructor>
auto make_handle(Buffer buffer, Destructor dstr)
{ return std::shared_ptr<std::remove_pointer_t<Buffer>>(buffer, dstr); }
Usage:
auto h = make_handle(TF_AllocateBuffer(), TF_DeleteBuffer);
Full demo: https://coliru.stacked-crooked.com/a/b12e4adc559cbfd7
As a bonus, you can now copy the handle and it does The Right Thing:
{
auto h2 = h;
} // does not free h's buffer until h is out of scope :)
Upvotes: 1
Reputation: 10700
To add to the accepted answer:
Depending on your use-case, using a type trait
-based approach (similar to std::allocator
) would be a cleaner solution.
(Especially if you have many different handle types that you need to wrap with handle<>
)
Example:
// Boilerplate.
// Assume TF_Buffer and Foobar_Buffer would be handle types
struct TF_Buffer {};
struct Foobar_Buffer {};
// TF_Buffer functions
TF_Buffer* TF_AllocateBuffer() { return new TF_Buffer(); };
void TF_DeleteBuffer(TF_Buffer* buf) { delete buf; }
// Foobar_Buffer functions
Foobar_Buffer* Foobar_AllocateBuffer() { return new Foobar_Buffer(); };
void Foobar_DeleteBuffer(Foobar_Buffer* buf) { delete buf; }
// Generic handle_allocator for all handles that are not specified.
// if you don't have a generic way of allocating handles simply leave them out,
// which will lead to a compile-time error when you use handle<> with a non-specialized type.
template<typename handle_type>
struct handle_allocator {
/*
static handle_type* allocate() {
// Generic handle allocate
}
static void deallocate(handle_type* handle) {
// Generic handle delete
}
*/
};
// Traits for TF_Buffer
template<>
struct handle_allocator<TF_Buffer> {
static TF_Buffer* allocate() { return TF_AllocateBuffer(); }
static void deallocate(TF_Buffer* handle) { TF_DeleteBuffer(handle); }
};
// Traits for Foobar_Buffer
template<>
struct handle_allocator<Foobar_Buffer> {
static Foobar_Buffer* allocate() { return Foobar_AllocateBuffer(); }
static void deallocate(Foobar_Buffer* handle) { Foobar_DeleteBuffer(handle); }
};
template<typename Obj, typename allocator = handle_allocator<Obj>>
class handle {
public:
Obj* obj;
// you can also use the traits to default-construct a handle
handle() : obj(allocator::allocate()) {}
handle(Obj* o): obj(o) {};
~handle() { if (obj) allocator::deallocate(obj); }
};
class buffer : public handle<TF_Buffer> {
public:
buffer(TF_Buffer* b): handle(b) {}
};
// This will not work, because the generic handle_allocator
// doesn't have allocate() and deallocate() functions defined
/*
struct NotWorking {};
handle<NotWorking> w;
*/
Upvotes: 0
Reputation: 172924
You can define a non-type template parameter as function pointer.
template<typename Obj, void(*Deleter)(Obj*)>
class handle {
public:
Obj* obj;
handle(Obj* o): obj(o) {};
~handle() { if (obj) Deleter(obj); }
};
And use it like
class buffer : public handle<TF_Buffer, &TF_DeleteBuffer> {
...
};
Upvotes: 6