Search code examples
c++c++11template-meta-programmingdependent-name

Computing the type of a function pointer


Consider the following:

template<typename T>
struct S
{
    typedef M< &T::foo > MT;
}

This would work for:

S<Widget> SW;

where Widget::foo() is some function

How would I modify the definition of struct S to allow the following instead:

S<Widget*> SWP;

Solution

  • What you need is the following type transformation.

    • given T, return T
    • given T *, return T

    It so happens that the standard library already has implemented this for us in std::remove_pointer (though it's not hard to do yourself).

    With this, you can then write

    using object_type = std::remove_pointer_t<T>;
    using return_type = /* whatever foo returns */;
    using MT = M<object_type, return_type, &object_type::foo>;
    

    Regarding your comment that you also want to work with smart pointers, we have to re-define the type transformation.

    • given a smart pointer type smart_ptr<T>, return smart_ptr<T>::element_type, which should be T
    • given a pointer type T *, return T
    • otherwise, given T, return T itself

    For this, we'll have to code our own meta-function. At least, I'm not aware of anything in the standard library that would help here.

    We start by defining the primary template (the “otherwise” case).

    template <typename T, typename = void>
    struct unwrap_obect_type { using type = T; };
    

    The second (anonymous) type parameter that is defaulted to void will be of use later.

    For (raw) pointers, we provide the following partial specialization.

    template <typename T>
    struct unwrap_obect_type<T *, void> { using type = T; };
    

    If we'd stop here, we'd basically get std::remove_pointer. But we'll add an additional partial specialization for smart pointers. Of course, we'll first have to define what a “smart pointer” is. For the purpose of this example, we'll treat every type with a nested typedef named element_type as a smart pointer. Adjust this definition as you see fit.

    template <typename T>
    struct unwrap_obect_type
    <
      T,
      std::conditional_t<false, typename T::element_type, void>
    >
    {
      using type = typename T::element_type;
    };
    

    The second type parameter std::conditional_t<false, typename T::element_type, void> is a convoluted way to simulate std::void_t in C++14. The idea is that we have the following partial type function.

    • given a type T with a nested typedef named element_type, return void
    • otherwise, trigger a substitution failure

    Therefore, if we are dealing with a smart pointer, we'll get a better match than the primary template and otherwise, SFINAE will remove this partial specialization from further consideration.

    Here is a working example. T.C. has suggested using std::mem_fn to invoke the member function. This makes the code a lot cleaner than my initial example.

    #include <cstddef>
    #include <functional>
    #include <iostream>
    #include <memory>
    #include <string>
    #include <utility>
    
    template <typename ObjT, typename RetT, RetT (ObjT::*Pmf)() const noexcept>
    struct M
    {
      template <typename ThingT>
      static RetT
      call(ThingT&& thing) noexcept
      {
        auto wrapper = std::mem_fn(Pmf);
        return wrapper(std::forward<ThingT>(thing));
      }
    };
    
    template <typename T, typename = void>
    struct unwrap_obect_type { using type = T; };
    
    template <typename T>
    struct unwrap_obect_type<T *, void> { using type = T; };
    
    template <typename T>
    struct unwrap_obect_type<T, std::conditional_t<false, typename T::element_type, void>> { using type = typename T::element_type; };
    
    template <typename T>
    struct S
    {
    
      template <typename ThingT>
      void
      operator()(ThingT&& thing) const noexcept
      {
        using object_type = typename unwrap_obect_type<T>::type;
        using id_caller_type          = M<object_type, int,                &object_type::id>;
        using name_caller_type        = M<object_type, const std::string&, &object_type::name>;
        using name_length_caller_type = M<object_type, std::size_t,        &object_type::name_length>;
        std::cout << "id:          " << id_caller_type::call(thing)          << "\n";
        std::cout << "name:        " << name_caller_type::call(thing)        << "\n";
        std::cout << "name_length: " << name_length_caller_type::call(thing) << "\n";
      }
    
    };
    
    class employee final
    {
    
     private:
    
      int id_ {};
      std::string name_ {};
    
     public:
    
      employee(int id, std::string name) : id_ {id}, name_ {std::move(name)}
      {
      }
    
      int                  id()          const noexcept { return this->id_; }
      const std::string&   name()        const noexcept { return this->name_; }
      std::size_t          name_length() const noexcept { return this->name_.length(); }
    
    };
    
    int
    main()
    {
      const auto bob = std::make_shared<employee>(100, "Smart Bob");
      const auto s_object = S<employee> {};
      const auto s_pointer = S<employee *> {};
      const auto s_smart_pointer = S<std::shared_ptr<employee>> {};
      s_object(*bob);
      std::cout << "\n";
      s_pointer(bob.get());
      std::cout << "\n";
      s_smart_pointer(bob);
    }