2016-03-22 11 views
2

Einige allgemeine Code-Manipulationsfunktionen müssen je nachdem, ob eine Funktion einen Rückgabewert hat oder nicht, unterschiedlich funktionieren. Zum Beispiel, ein Problem von this question ausleihend, sagen wir, dass wir eine time_it Funktion schreiben müssen, nimmt eine Funktion und einige Argumente, führt sie aus und druckt die verstrichene Zeit aus. Der folgende Code kann dies tun:Vermeidung von Wiederholungen bei SFINAE Unterscheidung zwischen ungültigen und nicht voiden Rückgabetypen

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

Wie man sehen kann, gibt es einen Unterschied zwischen den Fällen der Funktion einen Wert zurückkehrt oder nicht. Im ersten Fall muss der Wert gespeichert, die verstrichene Zeit gedruckt und der Wert zurückgegeben werden. Im letzteren Fall muss nach dem Drucken der verstrichenen Zeit nichts mehr getan werden.

Die Frage ist, wie man mit beiden Fällen beschäftigen:

  1. Der obige Code verwendet std::enable_if und is_void, aber die erste (umständlich in sich selbst) Argument zu is_void wird als letztes Argument enable_if wiederholt - das ist umständlich und smells, esp. so viel vom Körper wird wiederholt.

  2. Die vorgenannte Antwort umgeht das Problem, indem die verstrichene Zeit als Nebenprodukt eines Destruktors einer bestimmten Klasse der verstrichenen Zeitgeber ausgegeben wird. Es ist eine nette Idee, aber in komplexeren Anwendungen würde zu verschachtelten Code führen (wesentliche Arbeit wird in einem Destruktor einer separaten Klasse getan - es ist kein natürlicher Fluss).

Gibt es einen besseren Weg, dies zu tun?

Antwort

3

Sie konnten die invoke-and-Shop-Code isolieren:

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

jetzt Ihr Code wird:

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

die jetzt hat der gleiche Körper für die beiden Fälle. Ich habe das Problem des Speicherns des Rückgabewerts (oder nicht) in einem Helfer ausgeklammert, so dass der Code, der damit umgehen will, sich nicht darum kümmern muss.

Ich fügte auch chain, die ein Funktionsobjekt und übergibt es entweder den vorherigen Rückgabewert als erstes Argument, oder nicht, je nachdem, ob der vorherige Rückgabewert ungültig war. Ich finde das Muster ziemlich häufig in Monad/Funktor-ähnlichem Code.

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)(...) Anrufe a() dann b(a(),...) oder a() dann b(...) je nachdem, welche a() zurückkehrt.

+0

Sehr allgemeines Refactoring - Thanks! –

+0

@AmiTavory Es wurde geändert, um 'std :: experimental :: optional 'zu verwenden, das durch' boost :: optional 'ersetzt werden kann, da ich aus Sicherheitsgründen Bedenken hatte. – Yakk

4

alles, was Sie manchmal brauchen, ist ein einfacher Tag-Typ:

template <class > struct tag { }; 

Sie können Ihre time_it basierend auf einem eingewickelt Ergebnisart Versandfertig:

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

Und wir haben dann nur Überlastungen für die void und nicht void Versionen:

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

Natürlich wäre es besonders schön, wenn regular void genehmigt wird - an diesem Punkt würden wir gar nicht erst den Sonderfall brauchen!

+0

Danke auch für den Link zur regulären void! –

2

Vielleicht würde eine Hilfsstruktur das tun?

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

Und Nutzung:

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

Gute Idee - danke! –

+1

'Fn (declltype (std :: vorwärts (args)) ...)' -> 'Fn (Args ...)' –

+2

Benötigt nicht 'decltype (std :: vorwärts (args)) .. .) 'das ist nur' Args && ... ' – Barry