Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compile time dispatch: conditional on valid call

Tags:

c++

c++11

sfinae

Given the following piece of code:

template<typename GroupA, typename GroupB>
class JoinedObjectGroup
   : public _ObjectSpaceHolder<GroupA>
   , public _ObjectSpaceHolder<GroupB>
   {
   public:
      JoinedObjectGroup(GroupA &groupA, GroupB &groupB)
         : _ObjectSpaceHolder<GroupA>(groupA)
         , _ObjectSpaceHolder<GroupB>(groupB)
         {
         }

      template<typename ObjectType>
      ObjectType get()
         {
            // Dispatch to appropriate handler: only one of the following actually compiles as
            // either GroupA knows about ObjectType or GroupB, but not both. So:
            //
         // return static_cast<_ObjectSpaceHolder<GroupA> &>(*this).m_objectSpace.get<ObjectType>();
            //  or
            // return static_cast<_ObjectSpaceHolder<GroupB> &>(*this).m_objectSpace.get<ObjectType>();
         }
   };

In the get() call, I'd like to perform a compile time dispatch to the appropriate handler. The underlying idea is that ObjectType is known either by GroupA or by GroupB. My initial approach was the following:

template<typename ObjectType>
ObjectType get()
    {
    return Dispatch<ObjectType, GroupA, GroupB>::get(*this);
    }

with:

template<typename ObjectType, typename GroupA, typename GroupB, typename = void>
struct Dispatch;

template<typename ObjectType, typename GroupA, typename GroupB>
struct Dispatch<ObjectType, GroupA, GroupB, typename std::enable_if<std::is_same<ObjectType, decltype(std::declval<GroupA>().template get<ObjectType>())>::value>::type>
   {
   template<typename JoinedGroup>
   static
   ObjectType get(JoinedGroup &joinedGroup)
      {
      return static_cast<_ObjectSpaceHolder<GroupA> &>(joinedGroup).m_objectSpace.get<ObjectType>();
      }
   };

template<typename ObjectType, typename GroupA, typename GroupB>
struct Dispatch<ObjectType, GroupA, GroupB, typename std::enable_if<std::is_same<ObjectType, decltype(std::declval<GroupB>().template get<ObjectType>())>::value>::type>
   {
   template<typename JoinedGroup>
   static
      ObjectType get(JoinedGroup &joinedGroup)
      {
      return static_cast<_ObjectSpaceHolder<GroupB> &>(joinedGroup).m_objectSpace.get<ObjectType>();
      }
   };

I had assumed this would work thinking that substituting ObjectType in the is_same clause of enable_if would lead one of the expressions to fail and therefore leaving only a single valid specialization. However, ambiguous names and re-definition errors prove me wrong.

Why is my reasoning incorrect? And how can I properly dispatch the call instead?

like image 768
dgrine Avatar asked Oct 18 '22 10:10

dgrine


2 Answers

namespace details {
  template<template<class...>class Z, class always_void, class...Ts>
  struct can_apply : std::false_type {};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, std::void_t<Z<Ts...>>, Ts...> : std::true_type {};
}
template<template<class...>class Z, class...Ts>
using can_apply = details::can_apply<Z, void, Ts...>;

this takes a template and a list of arguments, and tells you if you can apply them.

A template that evaluates foo.get<Bar>():

template<class ObjectType, class Source>
using get_template_result = decltype( std::declval<Source>().get<ObjectType>() );

Can we invoke the above template validly?

template<class ObjectType, class Source>
using can_get_template = can_apply< get_template_result, ObjectType, Source >;

A package to put a template into a type, that lets us evaluate it:

template<template<class...>class Z>
struct z_template {
  template<class...Ts>
  using result = Z<Ts...>;
};

A similar package, that discards its arguments and returns Result always:

template<class Result>
struct z_identity {
  template<class...>using result=Result;
};

Evaluates get_template_result if possible. If so, compares its type with ObjectType. Otherwise, compares ObjectType* with ObjectType (guaranteed false):

template<class ObjectType, class Source>
using get_template_gets_type = std::is_same<ObjectType,
  typename // maybe?
  std::conditional_t<
    can_get_template<ObjectType,Source>,
    z_template<get_template_result>,
    z_identity<ObjectType*>
  >::template result<ObjectType, Source>
>;

Once we have all that, we can tag dispatch!

template<class ObjectType, class T0, class...Ts, class Source>
ObjectType get_smart( Source&& source, std::true_type ) {
  return static_cast<T0&&>(std::forward<Source>(source)).get<ObjectType>();
}
template<class ObjectType, class T0, class T1, class...Ts, class Source>
ObjectType get_smart( Source&& source, std::false_type ) {
  return get_smart<ObjectType, T1, Ts...>(std::forward<Source>(source), get_template_gets_type<ObjectType, T1>{} );
}
template<class ObjectType, class T0, class...Ts, class Source>
ObjectType get_smart( Source&& source ) {
  return get_smart( std::forward<Source>(source), get_template_gets_type<ObjectType, T0>{} );
}

now get_smart<ObjectType, TypeA, TypeB>( something ) will search the list TypeA then TypeB until it finds a type you can call .get<ObjectType>() on and returns ObjectType. Then it stops.

If no such type is found, it fails to compile.

You are responsible to set the r/l value-ness of the list of types TypeA TypeB and of ObjectType. The length of the list is bounded by template recursion limits (usually in the 100s).

like image 199
Yakk - Adam Nevraumont Avatar answered Oct 21 '22 06:10

Yakk - Adam Nevraumont


If you can you use C++14, static_if looks like a clean solution:

template<typename ObjectType>
auto get()
{
    using is_group_a = std::is_same
    <
        ObjectType, 
        decltype(std::declval<GroupA>().template get<ObjectType>())
    >;

    return static_if(is_group_a{})
        .then([](auto& x_this)
        {
            return static_cast<_ObjectSpaceHolder<GroupA> &>(x_this)
                .m_objectSpace.get<ObjectType>();
        })
        .else_([](auto& x_this)
        {
            return static_cast<_ObjectSpaceHolder<GroupB> &>(x_this)
                .m_objectSpace.get<ObjectType>();           
        })(*this);
}   

Both branches need to be parseable, but only the taken branch will actually be instantiated.

I have written a tutorial on static_if for Meeting C++ 2015. It should be sufficient to understand how it works and to write your own implementation.

I have also written an implementation here.

Both implementation are based upon this CppCoreGuidelines issue.

like image 29
Vittorio Romeo Avatar answered Oct 21 '22 04:10

Vittorio Romeo