Предотвращение повторения для SFINAE Различение между типами возврата void и non void

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

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

2

Решение

Вы можете выделить код invoke-and-store:

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() возвращается.

3

Другие решения

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

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

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

4

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

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 {
//...
}
2
По вопросам рекламы [email protected]