Избегание повторения для SFINAE Различие между пустыми и непустыми типами возврата

Некоторый общий код, управляющий функциями, должен работать по-разному в зависимости от того, имеет функция возвращаемое значение или нет. Например, позаимствовав проблему из этого вопроса, скажем нам нужно написать функцию 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_if и is_void, но первый (громоздкий сам по себе) аргумент is_void повторяется как последний аргумент enable_if - это громоздко и запахи, особ. так как большая часть тела повторяется.

  2. Вышеупомянутый ответ обходит проблему, поскольку прошедшее время печатается как побочный продукт деструктора вызываемого класса некоторого истекшего таймера. Это хорошая идея, но в более сложных случаях это может привести к запутанному коду (существенная работа выполняется в деструкторе какого-то отдельного класса - это не естественный поток).

Есть ли лучший способ сделать это?


person Ami Tavory    schedule 22.03.2016    source источник


Ответы (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, который принимает объект функции и либо передает ему предыдущее возвращаемое значение в качестве первого аргумента, либо нет, в зависимости от того, было ли предыдущее возвращаемое значение недействительным. Я нахожу этот шаблон довольно распространенным в коде, подобном монаде/функтору.

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().

person Yakk - Adam Nevraumont    schedule 22.03.2016
comment
@AmiTavory Изменил его, чтобы использовать std::experimental::optional, который можно заменить на boost::optional, из-за проблем с безопасностью исключений, которые я заметил. - person Yakk - Adam Nevraumont; 22.03.2016

Иногда все, что вам нужно, это простой тип тега:

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

Конечно, было бы особенно хорошо, если бы обычный void получает одобрение, и в этот момент нам вообще не нужен специальный регистр!

person Barry    schedule 22.03.2016
comment
Спасибо также за ссылку на обычный void! - person Ami Tavory; 22.03.2016

Может быть, какая-то вспомогательная структура поможет?

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 {
  //...
}
person W.F.    schedule 22.03.2016
comment
Не нужно decltype(std::forward<Args>(args))...), это просто Args&&... - person Barry; 22.03.2016