避免重复SFINAE区分无效和非空返回类型

时间:2016-03-22 16:33:20

标签: c++ c++11 void sfinae result-of

一些通用代码操作函数,需要根据函数是否具有返回值而以不同方式操作。例如,从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);
}   

可以看出,函数返回值的情况之间存在差异。在前一种情况下,必须存储值,打印的经过时间和返回的值;在后一种情况下,在打印完经过的时间后,不需要再做任何事了。

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

  1. 上面的代码使用std::enable_ifis_void,但is_void的第一个(本身很麻烦)参数作为enable_if的最后一个参数重复 - 这很麻烦,smells,尤其是身体的多数重复。

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

  3. 有更好的方法吗?

3 个答案:

答案 0 :(得分:4)

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

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获得批准会特别好 - 在这一点上我们根本不需要特殊情况!

答案 1 :(得分:3)

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

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

现在两个案件的主体相同。我考虑了将返回值(或不存储)存储到帮助器中的问题,从而使得想要处理它的代码不必担心它。

我还包括chain,它接受​​一个函数对象,并将前一个返回值作为第一个参数传递给它,或者不传递它,具体取决于前一个返回值是否为void。我发现这种模式在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()返回的内容。

答案 2 :(得分:2)

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

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 {
  //...
}