2015-01-27 6 views
5

Sto provando a prendere un 'compito' nello stile di std::async e memorizzarlo in un contenitore. Devo saltare i cerchi per raggiungerlo, ma penso che ci debba essere un modo migliore.Come posso archiviare packaged_tasks generici in un contenitore?

std::vector<std::function<void()>> mTasks; 

template<class F, class... Args> 
std::future<typename std::result_of<typename std::decay<F>::type(typename std::decay<Args>::type...)>::type> 
push(F&& f, Args&&... args) 
{ 
    auto func = std::make_shared<std::packaged_task<typename std::result_of<typename std::decay<F>::type(typename std::decay<Args>::type...)>::type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...)); 
    auto future = func->get_future(); 

    // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: 
    mTasks.push_back([=, func = std::move(func)]{ (*func)(); }); 

    return future; 
} 

Così sto usando bind ->packaged_task ->shared_ptr ->lambda ->function. Come posso farlo meglio/in modo ottimale? Sarebbe sicuramente più semplice se ci fosse un std::function che potesse svolgere un'attività non trasferibile ma spostabile. Posso std :: inoltrare args nell'acquisizione di una lambda o devo usare bind?

+0

Perché non inserire l'attività pacchettizzata direttamente nel vettore? –

+1

@KerrekSB Perché 'packaged_task's non è copiabile e' std :: function' accetta solo cose che sono copiabili. – David

+1

Intendevo 'std :: vector >'. –

risposta

6

Non c'è uccisione come eccessivo.

Fase 1: scrivere uno SFINAE accogliente std::result_of e una funzione per aiutare chiamare via tuple:

namespace details { 
    template<size_t...Is, class F, class... Args> 
    auto invoke_tuple(std::index_sequence<Is...>, F&& f, std::tuple<Args>&& args) 
    { 
    return std::forward<F>(f)(std::get<Is>(std::move(args))); 
    } 
    // SFINAE friendly result_of: 
    template<class Invocation, class=void> 
    struct invoke_result {}; 
    template<class T, class...Args> 
    struct invoke_result<T(Args...), decltype(void(std::declval<T>()(std::declval<Args>()...))) > { 
    using type = decltype(std::declval<T>()(std::declval<Args>()...)); 
    }; 
    template<class Invocation, class=void> 
    struct can_invoke:std::false_type{}; 
    template<class Invocation> 
    struct can_invoke<Invocation, decltype(void(std::declval< 
    typename invoke_result<Inocation>::type 
    >()))>:std::true_type{}; 
} 

template<class F, class... Args> 
auto invoke_tuple(F&& f, std::tuple<Args>&& args) 
{ 
    return details::invoke_tuple(std::index_sequence_for<Args...>{}, std::forward<F>(f), std::move(args)); 
} 

// SFINAE friendly result_of: 
template<class Invocation> 
struct invoke_result:details::invoke_result<Invocation>{}; 
template<class Invocation> 
using invoke_result_t = typename invoke_result<Invocation>::type; 
template<class Invocation> 
struct can_invoke:details::can_invoke<Invocation>{}; 

Ora abbiamo invoke_result_t<A(B,C)> che è uno SFINAE accogliente result_of_t<A(B,C)> e can_invoke<A(B,C)> che fa proprio l'assegno.

Avanti, scrivere un move_only_function, una versione mossa solo di std::function:

namespace details { 
    template<class Sig> 
    struct mof_internal; 
    template<class R, class...Args> 
    struct mof_internal { 
    virtual ~mof_internal() {}; 
    // 4 overloads, because I'm insane: 
    virtual R invoke(Args&&... args) const& = 0; 
    virtual R invoke(Args&&... args) & = 0; 
    virtual R invoke(Args&&... args) const&& = 0; 
    virtual R invoke(Args&&... args) && = 0; 
    }; 

    template<class F, class Sig> 
    struct mof_pimpl; 
    template<class R, class...Args, class F> 
    struct mof_pimpl<F, R(Args...)>:mof_internal<R(Args...)> { 
    F f; 
    virtual R invoke(Args&&... args) const& override { return f(std::forward<Args>(args)...); } 
    virtual R invoke(Args&&... args)  & override { return f(std::forward<Args>(args)...); } 
    virtual R invoke(Args&&... args) const&& override { return std::move(f)(std::forward<Args>(args)...); } 
    virtual R invoke(Args&&... args)  && override { return std::move(f)(std::forward<Args>(args)...); } 
    }; 
} 

template<class R, class...Args> 
struct move_only_function<R(Args)> { 
    move_only_function(move_only_function const&)=delete; 
    move_only_function(move_only_function &&)=default; 
    move_only_function(std::nullptr_t):move_only_function() {} 
    move_only_function() = default; 
    explicit operator bool() const { return pImpl; } 
    bool operator!() const { return !*this; } 
    R operator()(Args...args)  & { return     pImpl().invoke(std::forward<Args>(args)...); } 
    R operator()(Args...args)const& { return     pImpl().invoke(std::forward<Args>(args)...); } 
    R operator()(Args...args)  &&{ return std::move(*this).pImpl().invoke(std::forward<Args>(args)...); } 
    R operator()(Args...args)const&&{ return std::move(*this).pImpl().invoke(std::forward<Args>(args)...); } 

    template<class F,class=std::enable_if_t<can_invoke<decay_t<F>(Args...)>> 
    move_only_function(F&& f): 
    m_pImpl(std::make_unique<details::mof_pimpl<std::decay_t<F>, R(Args...)>>(std::forward<F>(f))) 
    {} 
private: 
    using internal = details::mof_internal<R(Args...)>; 
    std::unique_ptr<internal> m_pImpl; 

    // rvalue helpers: 
    internal  & pImpl()  & { return *m_pImpl.get(); } 
    internal const& pImpl() const& { return *m_pImpl.get(); } 
    internal  && pImpl()  && { return std::move(*m_pImpl.get()); } 
    internal const&& pImpl() const&& { return std::move(*m_pImpl.get()); } // mostly useless 
}; 

non testato, solo vomitato il codice. Lo standard can_invoke fornisce il costruttore SFINAE di base - è possibile aggiungere "restituisce il tipo convertito correttamente" e "restituire il tipo restituito vuol dire ignorare il reso" se lo si desidera.

Ora rielaboriamo il codice. In primo luogo, il vostro compito sono funzioni Move-solo, non funzioni:

std::vector<move_only_function<X>> mTasks; 

Successivamente, memorizzare il calcolo R tipo di una volta, e riutilizzarlo:

template<class F, class... Args, class R=std::result_of_t<std::decay<F>_&&(std::decay_t<Args>&&...)>> 
std::future<R> 
push(F&& f, Args&&... args) 
{ 
    auto tuple_args=std::make_tuple(std::forward<Args>(args)...)]; 

    // lambda will only be called once: 
    std::packaged_task<R()> task([f=std::forward<F>(f),args=std::move(tuple_args)] 
    return invoke_tuple(std::move(f), std::move(args)); 
    }); 

    auto future = func.get_future(); 

    // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: 
    mTasks.emplace_back(std::move(task)); 

    return future; 
} 

abbiamo roba gli argomenti in una tupla, passa quella tupla in una lambda e invoca la tupla in un modo "solo fallo una volta" all'interno della lambda. Poiché invocheremo la funzione solo una volta, ottimizzeremo la lambda per quel caso.

Un packaged_task<R()> è compatibile con un move_only_function<R()> a differenza di un std::function<R()>, quindi possiamo semplicemente spostarlo nel nostro vettore. Il std::future che otteniamo dovrebbe funzionare bene anche se lo abbiamo ottenuto prima dello move.

Questo dovrebbe ridurre un po 'il sovraccarico. Certo, c'è un sacco di piatti.

Non ho compilato nessuno dei codici precedenti, l'ho appena estratto, quindi le probabilità che tutto sia compilato sono bassi. Ma gli errori dovrebbero essere principalmente tpyos.

A caso, ho deciso di assegnare a move_only_function 4 diversi sovraccarichi () (valore/valore lvalue e const/non). Avrei potuto aggiungere volatile, ma sembra spericolato. Quale aumento caldaia, certamente.

Anche il mio move_only_function non ha l'operazione "get at the stored stuff below" che ha std::function. Sentiti libero di digitare cancella se ti piace.E si tratta di (R(*)(Args...))0 come se fosse un puntatore a funzione reale (ho ritorno true quando il cast a bool, non come nulla: cancellazione del tipo di convert-to-bool potrebbe essere utile per una realizzazione di qualità più industriale

ho riscritto std::function. perché std manca un std::move_only_function, e il concetto, in generale, è un utile uno (come evidenziato da packaged_task). la soluzione rende il vostro mobile callable avvolgendolo con un std::shared_ptr.

Se non ti piace il boilerplate sopra, si consideri scrivendo make_copyable(F&&), che accetta una funzione oggetto F e lo avvolge usando la tua tecnica shared_ptr per renderlo riproducibile. È anche possibile aggiungere SFINAE per evitare di farlo se è già copiato (e chiamarlo ensure_copyable).

Quindi il codice originale sarebbe più pulito, come si farebbe semplicemente copiare packaged_task, quindi memorizzarlo.

template<class F> 
auto make_function_copyable(F&& f) { 
    auto sp = std::make_shared<std::decay_t<F>>(std::forward<F>(f)); 
    return [sp](auto&&...args){return (*sp)(std::forward<decltype(args)>(args)...); } 
} 
template<class F, class... Args, class R=std::result_of_t<std::decay<F>_&&(std::decay_t<Args>&&...)>> 
std::future<R> 
push(F&& f, Args&&... args) 
{ 
    auto tuple_args=std::make_tuple(std::forward<Args>(args)...)]; 

    // lambda will only be called once: 
    std::packaged_task<R()> task([f=std::forward<F>(f),args=std::move(tuple_args)] 
    return invoke_tuple(std::move(f), std::move(args)); 
    }); 

    auto future = func.get_future(); 

    // for some reason I get a compilation error in clang if I get rid of the `=, ` in this capture: 
    mTasks.emplace_back(make_function_copyable(std::move(task))); 

    return future; 
} 

questo richiede ancora il invoke_tuple boilerplate sopra, soprattutto perché non mi piace bind.

+0

Quindi i 2 problemi di root erano che non c'era 'move_only_function' e che non è possibile inoltrare argomenti variadici in una cattura lambda. Hai scritto un 'move_only_function' e inoltrato argomenti variadici in una cattura lambda usando una' tupla'. Touché. Ma la cosa 'tuple 'è più ottimale/migliore dell'uso di' bind', perché 'bind' è un po' meno lastra di gas. Inoltre, ecco la mia semplice intestazione solo la classe, con tutte queste cose; C++ dovrebbe supportare entrambi i problemi! – David

+0

@Dave sì. Una cosa bella è che "move_only_function" (o qualsiasi altra cosa tu voglia chiamarlo) è utile per altri scopi, e potresti andare a prendere i "delegati più veloci possibili" per renderlo più veloce della maggior parte delle implementazioni di 'std :: function'. E sì, 'bind' fa qualcosa di molto simile alla mia danza' tuple', ma non mi piace abbastanza 'bind' (ha troppa magia - lega un legame e ti strappa i capelli), e 'invoke_tuple' è abbastanza semplice (6-10 righe di codice?) Che sembra più pulito. – Yakk

+0

Abbassare il pensiero per usare 'bind' e ridurre qualche piastra (ma usare ancora' move_only_function', sarebbe: 'mTasks.push_back (std :: bind (std :: move (func), std :: forward (args) ...)); 'se ho fatto il' packaged_task' 'R (args ...)' invece di 'R()'. – David

Problemi correlati