Home > Software design >  Idiomatic C 11 for delegating template specialisations to a default implementation
Idiomatic C 11 for delegating template specialisations to a default implementation

Time:10-27

I'm making a struct Box<T> that handles some data. The specifics are unimportant.
An important note however is that Box<T> can store a pointer, but it might not. So both Box<int> and Box<int *> are valid. Obviously, if we own Box.data, we're going to need to delete data if it is a pointer type. Here's a solution I came up with that works in C 11:

template <typename T> struct BoxTraits;

template <typename T> struct Box {
    using traits_t = BoxTraits<T>;

    T data;

    ~Box() = default; // not required, I know

    T get_data() { return traits_t::get_data(this); }
};

template <typename T> struct Box<T *> {
    using traits_t = BoxTraits<T *>;

    T *data;

    ~Box() { delete data; }

    T *get_data() { return traits_t::get_data(this); }
};

template <typename T> struct BoxTraits {
    static T get_data(Box<T> *const box) { return box->data; }
};

Box::get_data is here to illustrate an issue with this design pattern. For every single method I want to add to Box, I need to add some boiler plate in each specialisation. Note that I would also need a Box<T *const> specialisation.

This seems like quite a rubbish solution. In C 14, I could use if constexpr with a is_ptr<T> trait and only have to write extra code in the methods that need specialising... Is there any way I can do this is in C 11?

This solution is shorter, cleaner and works for Box<U *const>!

template <typename T> struct is_ptr { static const bool value = false; };

template <typename U> struct is_ptr<U *> { static const bool value = true; };

template <typename U> struct is_ptr<U *const> {
    static const bool value = true;
};

template <typename T> struct Box {
    T data;

    ~Box() {
        if constexpr (is_ptr<T>::value) {
            delete data;
        }
    }

    T get_data() { return data; }
};

CodePudding user response:

First off, C 11 already has std::is_pointer, no need to roll your own. You can see that it inherits from std::true_type or std::false_type instead of defining its own value member. The reason for that is tag dispatching, that can effectively replace if constexpr in this situation:

template <typename T> struct Box {
    T data;

    ~Box() {
        destroy(std::is_pointer<T>{});
    }

private:
    void destroy(std::true_type) {
        delete data;
    }
    void destroy(std::false_type) {} // nothing to do
};

Demo

I think this is the most idiomatic way in C 11 for delegating to different implementations based on type traits. In many situations, tag dispatching can replace if constexpr (from C 17, not C 14), and I believe the latter always replaces the former in addition to being clearer. Tag dispatching can also be used before C 11 if you roll your own type traits.

Last note: you don't need to use the standard type traits, you can do something like this:

template <typename T> struct is_ptr { static const bool value = false; };
template <typename T> struct is_ptr<T*> { static const bool value = true; };
template <typename T> struct is_ptr<T* const> { static const bool value = true; };
template <typename T> struct is_ptr<T* volatile> { static const bool value = true; };
template <typename T> struct is_ptr<T* const volatile> { static const bool value = true; };

template<bool b>
struct bool_constant {};

template<typename T>
struct Box {
    T data;

    ~Box() {
        destroy(bool_constant<is_ptr<T>::value>{});
    }

private:
    void destroy(bool_constant<true>) {
        delete data;
    }
    void destroy(bool_constant<false>) {} // nothing to do
};

Demo

However, this pretty much amounts to recreating the standard type traits, but probably worse. Just use the standard library when possible.

CodePudding user response:

I think you had the right idea with the helper type, but I'd do it like the following example illustrates.

template <typename B, typename T>
struct BoxTraits {
    static T& get_data(B *const box) { return box->data; }
        //  ^--- important
    static T const& get_data(B const* const box) { return box->data; }
};
template <typename T>
struct BoxTraits<Box<T*>, T> {
    static T& get_data(Box<T*>* const box) { return *box->data; }
    static T const& get_data(Box<T*> const* const box) { return *box->data; }
};

Both versions always return T, so you can use them the same regardless of your Box's payload. You could add a type alias in Box so you don't have to pass the template arguments:

typedef Traits BoxTraits<Box, T>; // in Box class
  • Related