Search code examples
c++c++11voidsfinaeresult-of

Avoiding Repetition For SFINAE Differentiating Between void and Non-void Return Types


Some generic code manipulating functions, need to operate differently depending on whether a function has a return value or not. E.g., borrowing a problem from this question, say we need to write a time_it function takes a function and some arguments, runs it, and prints the elapsed time. The following code can do this:

#include <chrono>
#include <type_traits>
#include <cmath>
#include <iostream>

template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) ->  
    typename std::enable_if<
        !std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
        typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::type
{   
    const auto start = std::chrono::system_clock::now();
    auto const res = fn(std::forward<Args>(args)...);
    const auto end = std::chrono::system_clock::now();
    std::cout << "elapsed " << (end - start).count() << std::endl;
    return res;
}   

template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&...args) -> 
    typename std::enable_if<
        std::is_void<typename std::result_of<Fn(decltype(std::forward<Args>(args))...)>::type>::value,
        void>::type                                                                                                                                                                                      
{   
    const auto start = std::chrono::system_clock::now();
    fn(std::forward<Args>(args)...);
    const auto end = std::chrono::system_clock::now();
    std::cout << "elapsed " << (end - start).count() << std::endl;
}   

int main()
{   
    time_it([](double x){return std::cos(x);}, 3.0);
    time_it([](double x){}, 3.0);
}   

As can be seen, there is a difference between the cases of the function returning a value or not. In the former case, the value must be stored, the elapsed time printed, and the value returned; in the latter case, after printing the elapsed time, nothing more needs to be done.

The question is how to deal with both case:

  1. The above code uses std::enable_if and is_void, but the first (cumbersome in itself) argument to is_void is repeated as the last argument to enable_if - this is cumbersome and smells, esp. as much of the body is repeated.

  2. The aforementioned answer bypasses the problem by having the elapsed time being printed as the byproduct of a destructor of some elapsed-timer class being called. It's a nice idea, but in more complex uses would lead to convoluted code (substantial work is done in a destructor of some separate class - it's not a natural flow).

Is there a better way of doing this?


Solution

  • You could isolate the invoke-and-store code:

    template<class R>
    struct invoke_and_store_t {
      std::experimental::optional<R> ret;
      template<class F, class...Args>
      invoker_t&& operator()(F&& f, Args&&...args)&& {
        ret.emplace( std::forward<F>(f)(std::forward<Args>(args)...) );
        return std::move(*this);
      }
      R&& get()&&{ return std::move( *ret ) ); }
      template<class F>
      auto chain(F&& f)&&{
        return [r = std::move(*this).get(),f=std::move<F>(f)](auto&&...args)mutable
        {
          return std::move(f)(std::move(r), decltype(args)(args)...);
        };
      }
    };
    template<>
    struct invoke_and_store_t<void> {
      template<class F, class...Args>
      invoker_t&& operator()(F&& f, Args&&...args)&& {
        std::forward<F>(f)(std::forward<Args>(args)...);
        return std::move(*this);
      }
      void get()&&{}
      template<class F>
      auto chain(F&& f)&&{
        return [f=std::move<F>(f)](auto&&...args)mutable{
          return std::move(f)(decltype(args)(args)...);
        };
      }
    };
    template<class F, class...Args, class R=std::decay_t<std::result_of_t<F(Args...)>>>
    auto invoke_and_store(F&& f, Args&&...args) {
      return invoke_and_store_t<R>{}(std::forward<F>(f), std::forward<Arg>(args)...);
    }
    

    now your code becomes:

    template <class R, class Fn, class... Args>
    R time_it(tag<R>, Fn&& fn, Args&&... args)
    {
      const auto start = std::chrono::system_clock::now();
      auto&& res = invoke_and_store(
        std::forward<Fn>(fn), std::forward<Args>(args)...
      );
      const auto end = std::chrono::system_clock::now();
      std::cout << "elapsed " << (end - start).count() << std::endl;
      return std::move(res).get();
    }
    

    which now has the same body for the two cases. I factored out the problem of storing the return value (or not) into a helper, thus making the code that wants to deal with it not have to worry about it.

    I also included chain, which takes a function object and either passes it the previous return value as the first argument, or doesn't, depending on if the previous return value was void. I find that pattern pretty common in monad/functor-like code.

    template<class A, class B>
    auto then( A&& a, B&& b ) {
      return [a = std::forward<A>(a), B=std::forward<B>(b)](auto&&...args)mutable{
        return
          invoke_and_store(std::move(a))
          .chain(std::move(b))(decltype(args)(args)...);
      };
    }
    

    then(a,b)(...) calls a() then b(a(),...) or a() then b(...) depending on what a() returns.