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

拥有回忆 提交于 2019-12-08 05:38:23

问题


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?


回答1:


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
 }



回答2:


I suggest to use std::experimental::is_detected and then simply have:

template <typename T> dereferencable_type = decltype(*declval<T>());
template <typename T> arrow_type = decltype(declval<T>().operator->());

template <typename T> is_dereferencable =
    std::experimental::is_detected<dereferencable_type, T>;
template <typename T> has_arrow = std::experimental::is_detected<arrow_type, T>;

and then compose those traits:

template <typename T>
using is_pointer_like_dereferencable = is_dereferencable<T>;

template <typename T>
is_pointer_like_arrow_dereferencable = std::disjunction<std::is_pointer<T>, has_arrow<T>>;

template <typename T>
std::is_pointer_like = std::conjunction<is_pointer_like_dereferencable<T>,
                                        is_pointer_like_arrow_dereferencable<T>>;



回答3:


First off, this specialization is at best pointless, but actually wrong:

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

There isn't anything special about class template specializations with all type parameters when it comes to being pointer-like. So you shouldn't need to treat P<T, R...> any differently than just T.

Now, this specialization also has problems:

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

First, raw pointers don't have .operator->(). That makes the whole boolean expression ill-formed, which causes the entire specialization to get thrown out. Short circuiting happens on an expression by expression basis, but each expression still has to be valid.

Second, -> doesn't have to return add_pointer_t<T>. std::vector<X>::iterator::operator-> returns an X*, not an iterator*. So that's just checking the wrong thing.

We can let the base case check for pointers, and just let the specialization check for .operator-> across all types (whether they're class template specializations or not):

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

template <typename T>
struct is_pointer_like_arrow_dereferencable<T, 
    void_t<decltype(std::declval<T>().operator->())>
    >
: std::true_type
{ };

That said, is_pointer probably isn't sufficient since it doesn't make sense for int* to be "arrow dereferenceable". So you probably want to do something like:

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

template <typename T>
struct is_pointer_like_arrow_dereferencable<T*, void>
    : std::disjunction<std::is_class<T>, std::is_union<T>>
{ };

Or some such.




回答4:


Based on Jarod42's answer (which doesn't compile for me), I created the following code snippet (C++17),

#include "sys.h"    // Required for libcwd (debug output).
#include "debug.h"  // Required for libcwd (debug output).
#include <vector>
#include <memory>
#include <iostream>

//-----------------------------------------------------------------------------
// Start of implementation.

#include <experimental/type_traits>
#include <type_traits>

template<typename T> using dereferencable_type =
    decltype(*std::declval<T>());
template<typename T> using arrow_type =
    decltype(std::declval<T>().operator->());

template<typename T> using is_dereferencable =
    std::experimental::is_detected<dereferencable_type, T>;

template<typename T> using has_arrow =
    std::experimental::is_detected<arrow_type, T>;

template<typename T> using is_pointer_like_dereferencable =
    is_dereferencable<T>;

template<typename T> using is_pointer_like_arrow_dereferencable =
    std::disjunction<std::is_pointer<T>, has_arrow<T>>;

template <typename T> using is_pointer_like =
    std::conjunction<is_pointer_like_dereferencable<T>,
                     is_pointer_like_arrow_dereferencable<T>>;

template<typename T> inline constexpr bool is_pointer_like_dereferencable_v =
    is_pointer_like_dereferencable<T>::value;
template<typename T> inline constexpr bool is_pointer_like_arrow_dereferencable_v =
    is_pointer_like_arrow_dereferencable<T>::value;
template<typename T> inline constexpr bool is_pointer_like_v =
    is_pointer_like<T>::value;

// End of implementation
//-----------------------------------------------------------------------------

template<typename T>
struct TestNone
{
  static int const n;
};

template<typename T>
struct TestRef : public TestNone<T>
{
  T const& operator*() { return TestNone<T>::n; }
};

template<typename T>
struct TestArrow : public TestNone<T>
{
  TestArrow const* operator->() { return this; }
};

template<typename T>
struct TestBoth : virtual public TestRef<T>, virtual public TestArrow<T>
{
};

//static
template<typename T>
int const TestNone<T>::n = 42;

template<typename T>
void test()
{
  Dout(dc::notice|continued_cf, '"' <<
      libcwd::type_info_of<T>().demangled_name() << "\" is ");
  bool something = true;
  if (is_pointer_like_v<T>)
    Dout(dc::continued, "pointer-like.");
  else
  {
    if (is_pointer_like_arrow_dereferencable_v<T>)
      Dout(dc::continued, "arrow dereferencable; ");
    else
      something = is_pointer_like_dereferencable_v<T>;
    if (is_pointer_like_dereferencable_v<T>)
      Dout(dc::continued, "dereferencable; ");
  }
  if (!something)
    Dout(dc::continued, "not a pointer or pointer-like.");
  Dout(dc::finish, "");
}

int main()
{
  Debug(NAMESPACE_DEBUG::init());

  test<int>();
  test<int*>();
  test<std::vector<int>>();
  test<std::vector<int>*>();
  test<std::shared_ptr<int>>();
  test<std::shared_ptr<int>*>();
  test<std::shared_ptr<int***>>();
  test<TestNone<int>>();
  test<TestNone<int>*>();
  test<TestRef<int>>();
  test<TestRef<int>*>();
  test<TestArrow<int>>();
  test<TestArrow<int>*>();
  test<TestBoth<int>>();
  test<TestBoth<int>*>();
}

where sys.h and debug.h are 'standard' headers from the git submodule cwds.

This prints the following debug output:

NOTICE : "int" is not a pointer or pointer-like.
NOTICE : "int*" is pointer-like.
NOTICE : "std::vector<int, std::allocator<int> >" is not a pointer or pointer-like.
NOTICE : "std::vector<int, std::allocator<int> >*" is pointer-like.
NOTICE : "std::shared_ptr<int>" is pointer-like.
NOTICE : "std::shared_ptr<int>*" is pointer-like.
NOTICE : "std::shared_ptr<int***>" is pointer-like.
NOTICE : "TestNone<int>" is not a pointer or pointer-like.
NOTICE : "TestNone<int>*" is pointer-like.
NOTICE : "TestRef<int>" is dereferencable;
NOTICE : "TestRef<int>*" is pointer-like.
NOTICE : "TestArrow<int>" is arrow dereferencable;
NOTICE : "TestArrow<int>*" is pointer-like.
NOTICE : "TestBoth<int>" is pointer-like.
NOTICE : "TestBoth<int>*" is pointer-like.



来源:https://stackoverflow.com/questions/49904809/template-function-for-detecting-pointer-like-dereferencable-types-fails-for-ac

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!