danglingpointer
danglingpointer

Reputation: 712

Compile time dispatch: conditional on valid call

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?

Upvotes: 5

Views: 271

Answers (3)

Vittorio Romeo
Vittorio Romeo

Reputation: 93364

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.

Upvotes: 1

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275878

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).

Upvotes: 2

max66
max66

Reputation: 66230

What about

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

template<typename GroupA, typename GroupB>
struct Dispatch<GroupA, GroupA, GroupB>
   {
   template<typename JoinedGroup>
   static
   GroupA get(JoinedGroup &joinedGroup)
      {
      return static_cast<_ObjectSpaceHolder<GroupA> &>(joinedGroup).m_objectSpace.template get<GroupA>();
      }
   };

template<typename GroupA, typename GroupB>
struct Dispatch<GroupB, GroupA, GroupB>
   {
   template<typename JoinedGroup>
   static
      GroupB get(JoinedGroup &joinedGroup)
      {
      return static_cast<_ObjectSpaceHolder<GroupB> &>(joinedGroup).m_objectSpace.template get<GroupB>();
      }
   };

?

Your assumption seems rigth to me and I compile your code (adding a coulple of template; see the following "p.s.") but I think is over-complicated.

p.s.: the template before get() is requested by my clang++; my g++ does not require it but accepts it. I suppose you should add it to your version too.

p.s.2: sorry for my bad English.

--- EDIT ---

Thinking better, my solution is over-complicated too.

What about a simpler

  template<typename ObjectType>
  ObjectType get()
     {
       return static_cast<_ObjectSpaceHolder<ObjectType> &>(*this).m_objectSpace.template get<ObjectType>();
     }

?

If you intention is to be sure that ObjectType is GroupA or GroupB (and other types, if you wont extend the solution to other types) you could write something that say if a type is in a variadic list; something like

template <typename T0>
constexpr bool typeIsInList ()
 { return false; }

template <typename T0, typename T1, typename ... Tl>
constexpr bool typeIsInList ()
 { return std::is_same<T0, T1>::value || typeIsInList<T0, Tl...>(); }

and redefine get() to be sure (via SFINAE) that ObjectType is in the list constitued by GroupA and GroupB; something like

  template<typename ObjectType, typename = typename std::enable_if<typeIsInList<ObjectType, GroupA, GroupB>()>::type>
  ObjectType get()
     {
       return static_cast<_ObjectSpaceHolder<ObjectType> &>(*this).m_objectSpace.template get<ObjectType>();
     }

Upvotes: 0

Related Questions