Search code examples
c++c++11c++14enable-if

C++11/14: Wrap a function if it exists


I'd like to write a wrapper class (very much a proxy) that aggregates an object, and forwards member function calls to it. That's trivial in C++11/14 using variadic templates and decltype. My problem is that there are member functions that the wrapped object may or may not support.

I came up with a solution that seems to work, however, it looks extremely clumsy, and I'm looking for simplifications. In particular I'm afraid this might be extremely costly at compile time (there are many functions to wrap). This clumsiness comes from the need to specify the return type of the function without leaving to decltype something on which to choke.

Would someone have a better idea?

The following piece of code is also available live.

#include <iostream>
#include <utility>

/// Compute the result type of a member function call, or void if invalid.
#define RESULT_OF(Name)                                                 \
  template <typename T>                                                 \
  class result_impl_ ## Name                                            \
  {                                                                     \
  public:                                                               \
    /* Type made public to please Clang 3.7. */                         \
    template <typename C, typename... Args>                             \
      static auto Type(void*)                                           \
      -> decltype(std::declval<C>().Name(std::declval<Args>()...));     \
                                                                        \
    template <typename, typename...>                                    \
    static void Type(...);                                              \
                                                                        \
    template <typename... Args>                                         \
      using type = decltype(Type<T, Args...>(0));                       \
  };                                                                    \
                                                                        \
  template <typename T, typename... Args>                               \
  using maybe_result_of_ ## Name                                        \
    = typename result_impl_ ## Name<T>::template type<Args...>

/// Forward to function Name, if is exists.
#define FORWARD(Name)                                                   \
  template <typename... Args>                                           \
  auto Name(Args&&... args)                                             \
    -> maybe_result_of_ ## Name<Base, Args...>                          \
  {                                                                     \
    return base.Name(std::forward<Args>(args)...);                      \
  }

#define DEFINE(Name)                            \
  RESULT_OF(Name);                              \
  FORWARD(Name)


template <typename Base>
struct wrapper
{
  Base base;
  DEFINE(foo);
  DEFINE(bar);
};


#define PING()                                  \
  std::cerr << __PRETTY_FUNCTION__ << '\n'
struct foo_no_bar
{
  void foo(int)           const { PING(); }
  int foo(double)         const { PING(); return 1; }
  int foo(double, double) const { PING(); return 1; }
};

struct foo_and_bar
{
  void foo() const { PING(); }
  void bar()       { PING(); }
};

int main()
{
  wrapper<foo_and_bar> f;
  f.foo();
  f.bar();
  wrapper<foo_no_bar> b;
  b.foo(1);
  b.foo(1.0);
  b.foo(1.0, 2.0);
}

Solution

  • So I took a bunch of your work you do in a macro, and took it out.

    can_apply_t takes a template<class...>class and a pack of types, and is true if the types can be applied to the template legally.

    template<class...>struct voider { using type=void; };
    template<class...Ts>using void_t=typename voider<Ts...>::type;
    
    template<class...>struct types{ using type=types; };
    
    namespace details {
      template<template<class...>class Z, class types, class=void>
      struct can_apply : std::false_type {};
      template<template<class...>class Z, class...Ts>
      struct can_apply<Z, types<Ts...>, void_t< Z<Ts...> > >:
        std::true_type
      {};
    }
    
    template<template<class...>class Z, class...Ts>
    using can_apply_t = details::can_apply<Z, types<Ts...>>;
    

    Then we get to replacements for your macros. I decouple method names from object names, and do it in a few steps. These steps can be done outside of the class for each method:

    #define CALL_METHOD_RESULT(Name, Method) \
    template<class Sig, class=void> \
    struct Name {}; \
    template<class C, class...Ts> \
    struct Name< C(Ts...), void_t< \
      decltype( std::declval<C>().Method(std::declval<Ts>()...) ) \
    >> { \
      using type = decltype( std::declval<C>().Method(std::declval<Ts>()...) ); \
    }; \
    template<class Sig> \
    using Name ## _t = typename Name< Sig >::type
    

    The above defines Name_t which is a traits class that takes a Object(Args...) and tells you the return type of Object.Method(Args...) in a SFINAE friendly context.

    Next, we build a can_call_Method helper template using this, and the above can_apply_t:

    #define CAN_CALL_METHOD( Method ) \
    CALL_METHOD_RESULT( call_ ## Method, Method ); \
    template<class Sig> \
    using can_call_ ## Method = can_apply_t< call_ ## Method ## _t, Sig >
    

    Next, a FORWARD macro that generates a pair of overloads for Name, one of which calls target.Method(Args...) where Target target, and the other one is only considered if the first is not and explicitly =deletes the call to generate a possibly better error message.

    /// Forward to function Name, if is exists.
    #define FORWARD(Name, Method, Target, target) \
      template <class... Args> \
      auto Name(Args&&... args) \
      -> call_ ## Method ## _t<Target(Args...)> \
      { \
        return target.Method(std::forward<Args>(args)...); \
      } \
      template <class...Args> \
      std::enable_if_t<!can_call_ ## Method <Target(Args...)>{}> \
      Name ( Args&&...) = delete
    

    Now we wrap up the above in a similar DEFINE macro. Here we recouple everything back to the one name:

    #define DEFINE(Name) \
      CAN_CALL_METHOD(Name); \
      FORWARD(Name, Name, Base, base)
    

    live example

    We can also make the =delete method do something else, such as nothing, if you want.

    Note, however, if we omit the entire =delete method, we can actually do controlled dispatch between two sub-objects (even with priority!)

    An easier way to do all of this is of course

    Base* operator->(){ return &base; }  
    Base const* operator->()const{ return &base; }  
    

    which lets you access Base as wrapper->foo(whatever). It does expose everything about Base.