避免重复 SFINAE 区分 void 和 Non-void Return 类型

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

一些通用代码操作函数,需要根据函数是否具有 return 值进行不同的操作。例如,从 this question 借用一个问题,假设我们需要编写一个 time_it 函数接受一个函数和一些参数,运行它,并打印经过的时间。下面的代码可以做到这一点:

#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);
}   

可以看出,函数return赋值和不赋值的情况是有区别的。在前一种情况下,必须存储该值,打印经过的时间,并将值returned;在后一种情况下,打印经过的时间后,无需再做任何事情。

问题是如何处理这两种情况:

  1. 上面的代码使用了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,特别是。 body 重复了多少。

  2. 上述答案通过将经过的时间打印为某些 elapsed-timer class 的析构函数的副产品来绕过问题。这是个好主意,但在更复杂的用途中会导致复杂的代码(大量工作是在一些单独的 class 的析构函数中完成的——这不是自然流程)。

有更好的方法吗?

有时您只需要一个简单的标签类型:

template <class > struct tag { };

您可以根据包装结果类型调度 time_it

template <class Fn, class... Args, class R = std::result_of_t<Fn&&(Args&&...)>>
R time_it(Fn fn, Args&&... args)
{
    return time_it(tag<R>{}, fn, std::forward<Args>(args)...);
}

然后我们只有 void 和非 void 版本的重载:

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 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, class... Args>
void time_it(tag<void>, Fn fn, Args&&... args)
{
    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;
}

当然,如果 regular void 获得批准会特别好 - 到那时我们甚至根本不需要特殊情况!

也许一些辅助结构可以解决这个问题?

template <class T>
struct enable_if_not_void: enable_if<!is_void<T>::value, T> { };

和用法:

template<class Fn, typename ...Args>
auto time_it(Fn fn, Args &&... args) -> typename enable_if_not_void<typename std::result_of<Fn(Args &&...)>::type>::type {
  //...
}

您可以隔离调用和存储代码:

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

现在您的代码变为:

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();
}

这两个案例现在具有相同的正文。我解决了将 return 值存储(或不存储)到助手中的问题,从而使想要处理它的代码不必担心它。

我还包括了 chain,它接受一个函数对象并将前一个 return 值作为第一个参数传递给它,或者不传递,这取决于前一个 return 值无效。我发现这种模式在 monad/functor-like 代码中很常见。

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)(...) 调用 a() 然后 b(a(),...)a() 然后 b(...) 取决于 a() returns.