2013-05-26 7 views
11

Questo è un problema di ottimizzazione semantica su cui ho lavorato negli ultimi due giorni e sono bloccato. Il mio vero programma gira su un RTOS (FreeRTOS, in particolare), e ho bisogno di generare attività (che sono versioni semplificate e non terminanti di thread). L'API C prende uno void (*)(void*) per il punto di ingresso dell'attività e un parametro void*. Abbastanza standard.Come avere un puntatore a una funzione con argomenti arbitrari come parametro di modello?

Ho scritto una classe wrapper per un'attività e piuttosto che eseguire una delle implementazioni della vecchia scuola come avere un metodo virtuale che deve essere sovrascritto dalla classe task finale, preferirei che C++ generasse il le necessarie funzioni di memorizzazione dei parametri e di colla tramite modelli e funzioni variadici.

Ho fatto questo con lambdas e std::function e std::bind già, ma sembrano implementare un po 'gonfio, vale a dire non risolvendo la funzione obiettivo fino al runtime. In pratica, lo stesso meccanismo utilizzato da un metodo virtuale. Sto cercando di tagliare tutto il sovraccarico che posso, se possibile. Il bloat è arrivato a circa 200 byte per istanza in più rispetto all'implementazione codificata. (Questo è su un ARM Cortex-M3 con 128K di flash totale, e abbiamo solo circa 500 byte rimasti.) Tutte le domande SO che ho trovato sull'argomento rimandano in modo simile la risoluzione della funzione fino al runtime.

L'idea è che il codice:

  1. Conservare le versioni degradate degli argomenti variadic in un oggetto allocato sul mucchio (questa è una semplificazione, un allocatore potrebbe essere utilizzato invece), e passare questo come parametro void*,
  2. passare una funzione chiamata isola generata come punto di ingresso, con la firma void(void*), che chiama la funzione di destinazione con i parametri memorizzati, e
  3. (questa è la parte che non riesco a capire) fare in modo che il compilatore deduca i tipi dell'elenco di argomenti dal targe La firma della funzione t, per seguire il principio Non ripetersi.
  4. Nota che il puntatore a funzione e dei suoi tipi di argomento sono noti e risolti al momento della compilazione, e l'attuale valori degli argomenti passati alla funzione non sono noti fino al runtime (perché includono cose come puntatori di oggetti e opzioni di configurazione runtime).

Nell'esempio che segue, devo istanziare uno dei compiti come Task<void (*)(int), bar, int> task_bar(100); quando avrei preferito scrivere Task<bar> task_bar(100); o Task task_bar<bar>(100); e avere la figura del compilatore out (o in qualche modo raccontarla in biblioteca) che gli argomenti variadic deve corrispondere alla lista degli argomenti della funzione specificata.

La risposta "ovvia" sarebbe una sorta di firma modello come template<typename... Args, void (*Function)(Args...)> ma, inutile dirlo, che non viene compilata. Né lo è il caso in cui Function è il primo argomento.

Non sono sicuro che questo sia possibile, quindi sto chiedendo qui per vedere cosa ne pensate voi. Ho omesso il codice della variante che indirizza i metodi oggetto invece delle funzioni statiche per semplificare la domanda.

Quanto segue è un caso di prova rappresentativo. Lo sto costruendo con gcc 4.7.3 e il flag -std=gnu++11.

#include <utility> 
#include <iostream> 
using namespace std; 

void foo() { cout << "foo()\n"; } 
void bar(int val) { cout << "bar(" << val << ")\n"; } 

template<typename Callable, Callable Target, typename... Args> 
struct TaskArgs; 

template<typename Callable, Callable Target> 
struct TaskArgs<Callable, Target> { 
    constexpr TaskArgs() {} 
    template<typename... Args> 
    void CallFunction(Args&&... args) const 
    { Target(std::forward<Args>(args)...); } 
}; 

template<typename Callable, Callable Target, typename ThisArg, 
    typename... Args> 
struct TaskArgs<Callable, Target, ThisArg, Args...> { 
    typename std::decay<ThisArg>::type arg; 
    TaskArgs<Callable, Target, Args...> sub; 
    constexpr TaskArgs(ThisArg&& arg_, Args&&... remain) 
    : arg(arg_), sub(std::forward<Args>(remain)...) {} 
    template<typename... CurrentArgs> 
    void CallFunction(CurrentArgs&&... args) const 
    { sub.CallFunction(std::forward<CurrentArgs>(args)..., arg); } 
}; 

template<typename Callable, Callable Target, typename... Args> 
struct TaskFunction { 
    TaskArgs<Callable, Target, Args...> args; 
    constexpr TaskFunction(Args&&... args_) 
    : args(std::forward<Args>(args_)...) {} 
    void operator()() const { args.CallFunction(); } 
}; 

// Would really rather template the constructor instead of the whole class. 
// Nothing else in the class is virtual, either. 
template<typename Callable, Callable Entry, typename... Args> 
class Task { 
public: 
    typedef TaskFunction<Callable, Entry, Args...> Function; 
    Task(Args&&... args): taskEntryPoint(&Exec<Function>), 
     taskParam(new Function(std::forward<Args>(args)...)) { Run(); } 
    template<typename Target> 
    static void Exec(void* param) { (*static_cast<Target*>(param))(); } 
    // RTOS actually calls something like Run() from within the new task. 
    void Run() { (*taskEntryPoint)(taskParam); } 
private: 
    // RTOS actually stores these. 
    void (*taskEntryPoint)(void*); 
    void* taskParam; 
}; 

int main() 
{ 
    Task<void (*)(), foo> task_foo; 
    Task<void (*)(int), bar, int> task_bar(100); 
    return 0; 
} 
+3

L'unica cosa che si può fare è 'Task ' con la configurazione corrente. La mia domanda è, però - perché il puntatore a funzione ha un parametro di modello? Se ti sbarazzi di questo requisito, puoi semplicemente "auto tfoo = make_task (foo);" con deduzione completa. – Xeo

+0

Si consiglia di guardare [qui] (http://stackoverflow.com/a/9045644/775806). –

+0

Perché usare i modelli quando typedef e un param faranno? –

risposta

4

Alcuni boilerplate metaprogrammazione per iniziare:

template<int...> struct seq {}; 
template<int Min, int Max, int... s> struct make_seq:make_seq<Min, Max-1, Max-1, s...> {}; 
template<int Min, int... s> struct make_seq<Min, Min, s...> { 
    typedef seq<s...> type; 
}; 
template<int Max, int Min=0> 
using MakeSeq = typename make_seq<Min, Max>::type; 

Helper per decomprimere una tupla:

#include <tuple> 
template<typename Func, Func f, typename Tuple, int... s> 
void do_call(seq<s...>, Tuple&& tup) { 
    f(std::get<s>(tup)...); 
} 

Tipo del puntatore a funzione risultante:

typedef void(*pvoidary)(void*); 

I workhors effettivi e. Si noti che nessuna funzione in testa virtuale si verifica:

template<typename FuncType, FuncType Func, typename... Args> 
std::tuple<pvoidary, std::tuple<Args...>*> make_task(Args&&... args) { 
    typedef std::tuple<Args...> pack; 
    pack* pvoid = new pack(std::forward<Args>(args)...); 
    return std::make_tuple(
    [](void* pdata)->void { 
     pack* ppack = reinterpret_cast<pack*>(pdata); 
     do_call<FuncType, Func>(MakeSeq<sizeof...(Args)>(), *ppack); 
    }, 
    pvoid 
); 
} 

Ecco una macro che rimuove alcuni decltype boilerplate. In C++ 17 (e forse 14) questo non dovrebbe essere richiesto, possiamo dedurre il primo argomento dal secondo:

#define MAKE_TASK(FUNC) make_task< typename std::decay<decltype(FUNC)>::type, FUNC > 

test harness:

#include <iostream> 

void test(int x) { 
    std::cout << "X:" << x << "\n"; 
} 
void test2(std::string s) { 
    std::cout << "S:" << s.c_str() << "\n"; 
} 
int main() { 
    auto task = MAKE_TASK(test)(7); 
    pvoidary pFunc; 
    void* pVoid; 
    std::tie(pFunc, pVoid) = task; 
    pFunc(pVoid); 
    delete std::get<1>(task); // cleanup of the "void*" 
    auto task2 = MAKE_TASK(test2)("hello"); 
    std::tie(pFunc, pVoid) = task2; 
    pFunc(pVoid); 
    delete std::get<1>(task2); // cleanup of the "void*" 
} 

Live version

E , per i posteri, la mia prima pugnalata, che è divertente, ma ho mancato il segno: Old version (Fa il bind di runtime della funzione da chiamare, con conseguente chiamate alla funzione voidary facendo due chiamate inevitabilmente)

Un piccolo raggiro - se non si è std::move gli argomenti nell'attività (o altrimenti si induce un move su quella chiamata, come usare i temporaries), si finirà con riferimenti a loro piuttosto che copie di loro nel void*.

+0

In realtà, ho avuto questo lavoro con lambda prima. È il caso di codice in cui sto cercando di migliorare, perché l'uso di lambdas porta a due cose: 1) qualcosa di efficacemente uguale a due classi generate dal compilatore con una funzione virtuale: un abstract con solo 'virtual void lambda (void) = 0' e la seconda classe finale con l'implementazione, e 2) la necessità di una funzione "throw bad_func" (anche con '-fno-exceptions'), simile a come devi definire' __cxa_pure_virtual' per gestire un puro virtuale chiamata. Sono riuscito a #pragma weak' # 2 per qualcosa che avevo, ma non il vtbl bloat dal numero 1. –

+0

BTW, la versione lambda è stata davvero semplice: il costruttore 'Task' ha appena preso una' std :: function 'che ha passato il parametro. Quindi il chiamante ha appena passato qualcosa come '[&]() {myTask (x, y, z);}'. –

+1

@MikeDeSimone L'ho riscritto, quello che è sopra è la versione 3. 'std :: function ' è stato eliminato nella versione 2, perché come hai notato è un overhead della funzione 'virtuale'. Lambda non significa 'std :: function' -' std :: function' è di tipo cancellato su Lambdas. Quanto sopra dovrebbe associare il puntatore alla funzione in fase di compilazione e passare gli argomenti in fase di esecuzione. Probabilmente dovrei restituire una 'std :: tupla <...> *' così l'eliminazione è sicura, permettimi di correggerla. – Yakk