Search code examples
c++templatesreturnc++17perfect-forwarding

How to perfect forward a value returned from a function while returning a second value?


I want to make a benchmark function that, given an invocable and its parameters, will time the execution of the given invocable and return the measured std::chrono::duration<...> as well as the value returned by the invocation of the invocable.

I am having problems with perfect forwarding the value returned from the invocation. Currently I return the value returned by the invocation and use a reference parameter to return the duration:

using bench_clock = std::conditional_t<std::chrono::high_resolution_clock::is_steady, 
                      std::chrono::high_resolution_clock, std::chrono::steady_clock>;

decltype(auto) benchmark(
   bench_clock::duration& duration, auto&& func, auto&&... args)
{
   auto begin{ bench_clock::now() };
   decltype(auto) result{ 
      std::invoke(
         std::forward<decltype(func)>(func), 
         std::forward<decltype(args)>(args)...) 
   };
   duration = bench_clock::now() - begin;
   return result;
}

As far as I know, this perfectly forwards the value returned from the invocation.

I would prefer to also return the duration conventionally, as an example, by using std::tuple though I am not sure how to do it t perfectly forward the returned value.

My guess would be to use std::invoke_result_t like this:

using bench_clock = std::conditional_t<
   std::chrono::high_resolution_clock::is_steady, 
   std::chrono::high_resolution_clock, std::chrono::steady_clock>;

auto benchmark(auto&& func, auto&&... args)
{
   auto begin{ bench_clock::now() };
   decltype(auto) result{
      std::invoke(
         std::forward<decltype(func)>(func),
         std::forward<decltype(args)>(args)...) 
   };
   auto duration{ bench_clock::now() - begin };
   return std::tuple<std::invoke_result_t<decltype(func), decltype(args)...>, bench_clock::duration>{std::forward<decltype(result)>(result), duration};
   //------------------------------------------------------------------------------------------------^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ is this needed?
}

I am not sure if this approach correctly perfectly forwards. I also don't know if is required to use std::forward in the std::tuple constructor.

There is also a problem, that if the invocable returns void, the tuple cannot be used, as std::tuple<void> cannot be instantiated.

I am not sure how to go around this.


Solution

  • Yes, you are right. You need to handle the void return case when you invoke the function func inside the benchmark function.

    Since you have used std::invoke_result_t and std::conditional_t, I assume that you have access to . Then this can be easily solved by an if constexpr checking.

    #include <iostream>
    #include <chrono>
    #include <tuple>
    #include <type_traits>  // std::is_same_v,  std::invoke_result_t
    #include <functional>   // std::invoke
    #include <utility>      // std::forward, std::move
    
    using namespace std::chrono;
    using bench_clock = std::conditional_t<
       high_resolution_clock::is_steady, high_resolution_clock, steady_clock>;
    
    
    template<typename Func, typename... Args>
    decltype(auto) benchmark(Func&& func, Args&&... args)
    {
       auto begin{ bench_clock::now() };
       if constexpr (std::is_same_v<void, std::invoke_result_t<Func, Args...>>)
       {
           // can not have void result: therefore just invoke the func!
          std::invoke(std::forward<Func>(func), std::forward<Args>(args)...); 
          return bench_clock::now() - begin; // only the time duration will be returned!
       }
       else
       {
          // all other cases return the tuple of result-duration!
          decltype(auto) result{ std::invoke(std::forward<Func>(func), std::forward<Args>(args)...) };
          const auto duration{ bench_clock::now() - begin };
          return std::make_tuple(std::move(result), duration);
       }
    }
    

    (See a Demo Online Live)