c++templatesc++17template-meta-programmingsfinae

Template function for detecting pointer like (dereferencable) types fails for actual pointer types


I am trying to write a mechanism to detect if a type is a pointer like type. By that I mean it is dereferencable through operator*() and operator->().

I have three different structs that are specialized accordingly:

  1. is_pointer_like_dereferencable which checks for operator*()
  2. is_pointer_like_arrow_dereferencable which checks for operator->()
  3. is_pointer_like which simply combines 1 & 2

I added specializations for non-templated types like int, int*, ... and for templated types like std::vector<...>, std::shared_ptr<...>, ....

However, it seems that I made a mistake when implementing is_pointer_like_arrow_dereferencable. The relevant code is

template <typename T, typename = void>
struct is_pointer_like_arrow_dereferencable : std::false_type 
{
};

template <typename T>
struct is_pointer_like_arrow_dereferencable<T, std::enable_if_t<
                                                std::is_pointer_v<T> ||
                                                std::is_same_v<decltype(std::declval<T>().operator->()), std::add_pointer_t<T>>>
    > : std::true_type
{
};


template <template <typename...> typename P, typename T, typename... R>
struct is_pointer_like_arrow_dereferencable<P<T, R...>, std::enable_if_t<
                                                std::is_same_v<decltype(std::declval<P<T, R...>>().operator->()), std::add_pointer_t<T>>>
    > : std::true_type
{
};

template <typename T>
constexpr bool is_pointer_like_arrow_dereferencable_v = is_pointer_like_arrow_dereferencable<T>::value;

The 2nd struct should check if a non-templated type is either an actual pointer type or if the type does have an arrow operator that returns a pointer to itself. But when I test the mechanism with the code below, pointer types (like int* are not detected correctly). Why is that?

template <typename T>
struct Test
{
    T& operator*()
    {
        return *this;
    }

    T* operator->()
    {
        return this;
    }
};   

void main()
{
    bool
        a = is_pointer_like_arrow_dereferencable_v<int>, // false
        b = is_pointer_like_arrow_dereferencable_v<int*>, // false, should be true
        c = is_pointer_like_arrow_dereferencable_v<vector<int>>, // false
        d = is_pointer_like_arrow_dereferencable_v<vector<int>*>, // false, should be true
        e = is_pointer_like_arrow_dereferencable_v<Test<int>>, // true
        f = is_pointer_like_arrow_dereferencable_v<Test<int>*>, // false, should be true
        g = is_pointer_like_arrow_dereferencable_v<shared_ptr<int>>, // true
        h = is_pointer_like_arrow_dereferencable_v<shared_ptr<int>*>, // false, should be true
        i = is_pointer_like_arrow_dereferencable_v<int***>; // false
}

The is_pointer_like_dereferencable struct only differs at the std::is_same_v<...> part and it does detect actual pointer types correctly.

The fact that it fails to detect pointer types (which should be covered by std::is_pointer_v<...>) doesn't make any sense to me. Can someone explain this?


Solution

  • But when I test the mechanism with the code below, pointer types (like int* are not detected correctly). Why is that?

    S.F.I.N.A.E.: Substitution Failure Is Not An Error

    So, for int*, from decltype(std::declval<T>().operator->() you get a substitution failure and the specialization isn't considered. So is used the general form, so std::false

    You should write two specializations: one or pointers and one for operator->() enabled classes.

    Bonus answer: instead of type traits as is_pointer_like_arrow_dereferencable (overcomplicated, IMHO), I propose you to pass through a set of helper functions (only declared)

    template <typename>
    std::false_type is_pointer_like (unsigned long);
    
    template <typename T>
    auto is_pointer_like (int)
       -> decltype( * std::declval<T>(), std::true_type{} );
    
    template <typename T>
    auto is_pointer_like (long)
       -> decltype( std::declval<T>().operator->(), std::true_type{} );
    

    so is_pointer_like_arrow_dereferencable can be simply written as a using

    template <typename T>
    using is_pointer_like_arrow_dereferencable = decltype(is_pointer_like<T>(0));
    

    with helper is_pointer_like_arrow_dereferencable_v

    template <typename T>
    static auto const is_pointer_like_arrow_dereferencable_v
       = is_pointer_like_arrow_dereferencable<T>::value;
    

    The following is a full working example

    #include <type_traits>
    #include <iostream>
    #include <memory>
    #include <vector>
    
    template <typename>
    std::false_type is_pointer_like (unsigned long);
    
    template <typename T>
    auto is_pointer_like (int)
       -> decltype( * std::declval<T>(), std::true_type{} );
    
    template <typename T>
    auto is_pointer_like (long)
       -> decltype( std::declval<T>().operator->(), std::true_type{} );
    
    template <typename T>
    using is_pointer_like_arrow_dereferencable = decltype(is_pointer_like<T>(0));
    
    template <typename T>
    static auto const is_pointer_like_arrow_dereferencable_v
       = is_pointer_like_arrow_dereferencable<T>::value;
    
    
    template <typename T>
    struct Test
     {
       T & operator*  () { return *this; }
       T * operator-> () { return  this; }
     }; 
    
    int main()
     {
       std::cout << is_pointer_like_arrow_dereferencable_v<int>
          << std::endl, // false
       std::cout << is_pointer_like_arrow_dereferencable_v<int*>
          << std::endl, // true
       std::cout << is_pointer_like_arrow_dereferencable_v<std::vector<int>>
          << std::endl, // false
       std::cout << is_pointer_like_arrow_dereferencable_v<std::vector<int>*>
          << std::endl, // true
       std::cout << is_pointer_like_arrow_dereferencable_v<Test<int>>
          << std::endl, // true
       std::cout << is_pointer_like_arrow_dereferencable_v<Test<int>*>
          << std::endl, // true
       std::cout << is_pointer_like_arrow_dereferencable_v<std::shared_ptr<int>>
          << std::endl, // true
       std::cout << is_pointer_like_arrow_dereferencable_v<std::shared_ptr<int>*>
          << std::endl, // true
       std::cout << is_pointer_like_arrow_dereferencable_v<int***>
          << std::endl; // true
     }