Random Notes‎ > ‎

Smart Pointers to Dumb Objects

posted Dec 27, 2018, 6:30 AM by William Shallum   [ updated Dec 29, 2018, 8:14 PM ]
This entry was prompted by my annoyance that a generic deleter template I encountered in some codebase only worked for void return types (second one in below examples), while the deleter function I wanted to call had a status return code.

The usual pattern for resources/objects created by a C library is something like:

Object *obj_create(/* args */);
void obj_destroy(Object *);

If you're using C++ to interface to a C library, you might want to use smart pointers to ensure cleanup of the objects (i.e. call obj_destroy once the pointer goes out of scope). Let's see how many ways we can do that by using std::unique_ptr! Also, the goal here is to be: easy(-ish) to use, and with minimal overhead.

First the definitions of the free functions:

#include <stdio.h>
#include <stdlib.h>
#include <memory>

s *s_new() {
        s *p = (s *)calloc(1, sizeof(s));
        printf("s_new: %p\n", p);
        return p;
}

void s_free(s *p) {
        printf("s_free: %p\n", p);
        free(p);
}

int s_free_i(s *p) { // somehow returns a success code (which we ignore)
        printf("s_free_i: %p\n", p);
        free(p);
        return 0;
}

Then the first one: a specific deleter type that calls a hard-coded function

struct s_deleter {
        void operator()(s *p) {
                s_free(p);
        }
};

using up_del_struct = std::unique_ptr<s, s_deleter>;

The second one: a generic deleter type that takes a non-type template parameter of a specific function type

template <typename T, void (*del)(T *)>
struct generic_deleter_voidret {
        void operator()(T *p) {
                del(p);
        }
};

using up_del_generic_v = std::unique_ptr<s, generic_deleter_voidret<s, s_free>>;

This won't work when the function does not return void:

// error: could not convert template argument ‘s_free_i’ to ‘void (*)(s*)’
// using up_del_generic_i = std::unique_ptr<s, generic_deleter_voidret<s, s_free_i>>;

The third one: a generic deleter type similar to the above, but the deleter function's return type is now generic

template <typename T, typename R, R (*del)(T *)>
struct generic_deleter_anyret {
        void operator()(T *p) {
                del(p);
        }
};

using up_del_anyret = std::unique_ptr<s, generic_deleter_anyret<s, int, s_free_i>>;

The fourth one: a unique pointer that has a function pointer associated with it

using up_del_fn = std::unique_ptr<s, decltype(&s_free)>;

The fifth one: one you can't use yet because your compiler doesn't support C++17 yet (this was tested on the compiler explorer)

// C++17
#if __cpp_nontype_template_parameter_auto
template <typename T, auto deleter_fn>
struct generic_deleter_autofn {
        void operator()(T *p) {
                deleter_fn(p);
        }
};
using up_del_autofn = std::unique_ptr<s,  generic_deleter_autofn<s, s_free_i> >;
#endif

The sixth one: after learning that auto was added to save typing decltype (i.e. template<auto val> == template<typename T, T val> where T = decltype(val)), this is what I came up with:

template<typename> struct fn{};

template <typename Ret, typename Arg> // function pointer
struct fn<Ret(*)(Arg)>{ using ret = Ret; using arg = Arg; };

template <typename Del, Del del>
struct generic_deleter_fn {
        void operator()(typename fn<Del>::arg p) const { del(p); }
};

template<typename Del, typename std::decay<Del>::type del>
using up_del_genericfn = std::unique_ptr<
        typename std::remove_cv< typename std::remove_pointer< typename fn< decltype(del) >::arg >::type >::type,
        generic_deleter_fn<decltype(del), del>
>;

The fn...arg, remove_pointer, remove_cv chain is to get the plain argument type of the deleter function for use as the first template parameter to std::unique_ptr e.g. (void (*)(const somepointer *) -> const somepointer * -> const somepointer -> somepointer).

The std::decay is to ensure that the type of the second template parameter is Ret(*)(Arg) (pointer to function) instead of Ret(Arg) (just function). Otherwise we'd have to type something like up_del_genericfn<decltype(&func), func> instead of up_del_genericfn<decltype(func), func> (there cannot be a non-type parameter of type Ret(Arg)).

Now we can define the smart pointer type solely in terms of its deleter function. I'm not sure if I should be impressed or disgusted. Obviously the above only works for function pointers, not function objects. Since this is for wrapping C code, this will do just fine. Without auto for non-type template parameters there is no good way of typing this without mentioning the function twice, but that can be done only once in some type alias in a header file.

And how you use them:

int main() {
        {
                s *p = s_new();
                s_free(p);
        }
        {
                up_del_struct p(s_new());
                static_assert(sizeof(p) == sizeof(s *), "same size as raw pointer");
        }
        {
                up_del_generic_v p(s_new());
                static_assert(sizeof(p) == sizeof(s *), "same size as raw pointer");
        }
        {
                up_del_anyret p(s_new());
                static_assert(sizeof(p) == sizeof(s *), "same size as raw pointer");
        }
        {
                // cannot just create with pointer
                up_del_fn p(s_new(), s_free);
                // cannot just create an empty one either
                // up_del_fn empty;
                // larger, contains pointer to s_free

                static_assert(sizeof(p) > sizeof(s *), "larger than raw pointer");
        }
#if __cpp_nontype_template_parameter_auto
        {
                up_del_autofn p(s_new());
                static_assert(sizeof(p) == sizeof(s *), "same size as raw pointer");
        }
#endif
        {
                up_del_genericfn<decltype(s_free), s_free> p(s_new());
                up_del_genericfn<decltype(s_free_i), s_free_i> p2(s_new());
                static_assert(sizeof(p) == sizeof(s *), "same size as raw pointer");
                static_assert(sizeof(p2) == sizeof(s *), "same size as raw pointer");
        }

        return 0;
}

Output here (without the C++17 bit):

s_new: 0x2244010 // raw pointer
s_free: 0x2244010
s_new: 0x2244010 // up_del_struct
s_free: 0x2244010
s_new: 0x2244010 // up_del_generic_v
s_free: 0x2244010
s_new: 0x2244010 // up_del_anyret
s_free_i: 0x2244010
s_new: 0x2244010 // up_del_fn
s_free: 0x2244010
s_new: 0x2244010 // up_del_genericfn w/ s_free
s_new: 0x2244030 // up_del_genericfn w/ s_free_i
s_free_i: 0x2244030
s_free: 0x2244010

Of the alternatives above, the easiest one to use is definitely the C++17 version - just name the function and no need to think too much about the type. The generic one with std::decay and type deduction based on the deleter's function type isn't too bad to use, but rather horrible to write.

Other "brilliant" ideas include:
  1. Specializing std::default_delete - people say you shouldn't - not sure which part of the sentence below forbids it.
    "A program may add a template specialization for any standard library template to namespace std only if the declaration depends on a user-defined type and the specialization meets the standard library requirements for the original template and is not explicitly prohibited."
    I'm guessing it's the "meets requirements" thing, since obviously the specialization depends on a user defined type.
  2. Deriving from std::unique_ptr and overriding get_deleter - why not just create a usable deleter type - it's the same amount of work.
  3. Specializing std::unique_ptr - combines bad parts of brilliant ideas #1 and #2 above
  4. Your own wrapper type - would be fine if you needed more than this, but if you just want a deleter, people understand std::unique_ptr.
  5. Using std::function of some kind - works, but same problems as the function pointer one - larger and need to specify deleter on creation. This did somehow fix the issue with the deleter returning int, but maybe that's non standard?
Not covered here is what happens if the C deleter function has more than one parameter e.g. freeResource(resource_type, resource). I guess in C++ 17 you could use a constexpr lambda. Otherwise a small inline wrapper function could work.

References:
Comments