5

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.

5
  • 1
    I hit that recently and my conclusion was "nope". I'm pretty sure that your working solution is the one to use, and there are reasons to unique_ptr using that as well. Except that it also asks for the instance of the deleter, as opposed to creating a default one. Commented Jul 26, 2019 at 13:54
  • @akub Arnold Could you declare the class buffer as a template class? Commented Jul 26, 2019 at 13:54
  • Can the deleter function vary? If not you can just create your handle class without template parameters and call TF_DeleteBuffer in its destructor. Commented Jul 26, 2019 at 13:55
  • @VladfromMoscow yes I can define buffer to be anything, since then I can just store the specialization under a reasonable name Commented Jul 26, 2019 at 13:57
  • @MikeLischke there are many different combinations of "Obj" and "Deleter" so that's why the handle is made generic ... I basically want to wrap a whole bunch of C API with RAII Commented Jul 26, 2019 at 13:57

4 Answers 4

6

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> {
  ...
};
Sign up to request clarification or add additional context in comments.

7 Comments

Thanks, this is exactly what I was looking for! It seems the &TF_DeleteBuffer is not needed and just passing TF_DeleteBuffer works (I guess functions get casted to function pointers?) ... anyway, I guess shame on me for not knowing function syntax properly when trying this hehe
@JakubArnold Yes, for non-member functions and static member functions & is optional because of function-to-pointer decay.
I think a template alias (using buffer = handle<TF_Buffer, &TF_DeleteBuffer>) would be much simpler and cleaner to use here, if available, of course.
@JakubArnold Note: this is a demonstration snippet not fit for real-life program; you should modify it to respect the rule of 0/3/5 ;)
@Demolishun As template parameter, No.
|
1

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 :)

Comments

0

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;
*/

Example in Godbolt

Comments

0

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; 
       });

4 Comments

note: to define template-argument, the keywords class and typename are exact synonyms. Maybe choose one and stick to it.
@YSC that's correct. That's why there is no reason to care much about that
(joking) Are you familiar with my favourite quality indicator of source code: wtf/line?
@YSC one wtf per line is okay, 2 to 3 per line and we may have a problem?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.