2016-03-22 9 views
2
.

. Некоторые общие функции, управляющие кодом, должны работать по-разному в зависимости от того, имеет ли функция возвращаемое значение или нет. Например, заимствуя проблему от this question, скажем, что нам нужно написать функцию time_it, функция принимает и аргументирует, запускает ее и печатает прошедшее время. Следующий код может это сделать:Избегайте повторения для SFINAE. Дифференцирование между типами пустот и непустых типов.

#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 - это является громоздким и smells, особенно. так как большая часть тела повторяется.

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

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

ответ

3

Вы могли бы выделить код Invoke-и-магазин:

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

+0

Очень общий рефакторинг - спасибо! –

+0

@AmiTavory Изменил его, чтобы использовать 'std :: experimental :: optional', который можно заменить' boost :: optional', из-за проблем с безопасностью, которые я заметил. – Yakk

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 получит одобрение - в этот момент нам даже не понадобится специальный случай!

+0

Спасибо также за ссылку на обычную пустоту! –

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

Хорошая идея - спасибо! –

+1

'Fn (decltype (std: forward (args)) ...)' -> 'Fn (Args ...)' –

+2

Не нужно 'decltype (std :: forward (args)) .. .) 'это просто' Args && ... ' – Barry