risorse | delegates
Quel che segue è la cronaca di un tentativo di implementare i delegate in C++11. Con delegate intendo degli oggetti in grado di “catturare” una funzionalità e di attivarla in un secondo tempo, specificando nel sito di chiamata i parametri sui quali operare. L'intenzione è di riuscire a catturare qualunque oggetto callable, nella fattispecie:
L'infrastruttura è stata sviluppata con il compilatore GCC 4.8.2 su Xubuntu 14.04 e MinGW 4.8.2 su Windows 7 x64, utilizzando su entrambe le piattaforme le opzioni -Wall -Wpedantic -std=c++11.
Ecco alcuni esempi di funzionalità che si intende catturare:
// a free function int add(int a, int b) { return a + b; } // a function member struct Math { int add(int a, int b) { return a + b; } }; // a function object struct AddOp { int operator()(int a, int b) { return a + b; } };
Procediamo per gradi, affrontando inizialmente le funzioni semplici.
La funzionalità “dormiente” è conservata all'interno di un oggetto std::function, per la sua generalità (è infatti in grado di gestire sia funzioni semplici, sia funzioni lambda); sempre per generalità, questa volta rispetto al tipo degli argomenti del tipo del valore di ritorno della funzione originale, l'oggetto Delegate, che ha la responsabilità di catturare la funzionalità e di attivarla al momento giusto, fa uso di variadic templates (R rappresenta il tipo del valore di ritorno della funzione, Args l'elenco dei tipi degli argomenti):
#include <cassert> #include <functional> int add(int a, int b) { return a + b; } struct Math { static int add(int a, int b) { return a + b; } }; template<typename R, typename... Args> class Delegate { typedef std::function<R(Args...)> Fn; // function signature Fn fn_; // function container public: Delegate(Fn fn) : fn_(fn) { } R operator()(Args... args) const { // function activation return fn_(args...); } }; int main() { // wrapping a free function auto addFn = Delegate<int, int, int>(add); assert(addFn(1, 2) == 3); // wrapping a static function member auto staticAddFnMem = Delegate<int, int, int>(Math::add); assert(staticAddFnMem(1, 2) == 3); // bonus: delegates are copiable! auto copyOfAddFn = addFn; assert(copyOfAddFn(1, 2) == 3); return 0; }
Per attivare una funzione membro serve anche un riferimento all'istanza dell'oggetto sul quale invocare il metodo, che dovrà ovviamente risultare valida all'atto della chiamata — responsabilità demandata al programmatore: un nuovo oggetto di supporto, FnMemCall, si occupa di conservare i riferimenti necessari e di attivare il metodo designato sull'oggetto corretto:
template<class T> struct FnMemCall { T* obj_; // instance object typedef R (T::*Fn)(Args...); // function member signature Fn fn_; // function member reference // store object instance and function member for later FnMemCall(T* obj, Fn fn) : obj_(obj), fn_(fn) { } // invoke the function member on the given instance R operator()(Args... args) const { return (obj_->*fn_)(args...); } };
Un nuovo costruttore di Delegate istanzia l'FnMemCall ausiliario e lo pone in fn_, alla stregua di quanto già accade per le funzioni semplici:
// ... struct Math { static int add(int a, int b) { return a + b; } int diff(int a, int b) { return a - b; } }; // ... template<typename R, typename... Args> class Delegate { typedef std::function<R(Args...)> Fn; Fn fn_; template<class T> struct FnMemCall { T* obj_; typedef R (T::*Fn)(Args...); Fn fn_; FnMemCall(T* obj, Fn fn) : obj_(obj), fn_(fn) { } R operator()(Args... args) const { return (obj_->*fn_)(args...); } }; public: Delegate(Fn fn) : fn_(fn) { } template<class T> Delegate(T* obj, R (T::*fn)(Args...)) : fn_(FnMemCall<T>(obj, fn)) { } R operator()(Args... args) const { return fn_(args...); } }; int main() { // ... // wrapping a non-static function member Math math; auto diffFnMem = Delegate<int, int, int>(&math, &Math::diff); assert(diffFnMem(3, 2) == 1); return 0; }
Gli oggetti funzione possono essere trasformati in delegate applicando la tecnica dei metodi all'operatore function-call operator(), ma la forma risulta piuttosto prolissa:
// ... struct AddOp { int operator()(int a, int b) { return a + b; } }; int main() { // ... // wrapping a function object /1 AddOp addOp; auto addOpAsFnMem = Delegate<int, int, int>(&addOp, &AddOp::operator()); assert(addOpAsFnMem(1, 2) == 3); return 0; };
Grazie all'uso di un oggetto std::function, gli oggetti funzione possono essere catturati con una forma più compatta:
int main() { // ... // wrapping a function object /2 auto addOpAsFnObj = Delegate<int, int, int>(addOp); assert(addOpAsFnObj(1, 2) == 3); return 0; };
Questa tecnica può essere applicata anche alle funzioni lambda, per la loro affinità con gli oggetti funzione (essendo implementate come istanze di oggetti funzione anonimi con function-call operator const):
int main() { // ... // wrapping a lambda auto add_ = [](int a, int b) { return a + b; }; auto lambda = Delegate<int, int, int>(add_); assert(lambda(1, 2) == 3); // wrapping a temporary lambda auto lambda2 = Delegate<int, int, int>([](int a, int b) { return a + b; }); assert(lambda2(1, 2) == 3); return 0; };
Bello sarebbe poter evitare di specificare i tipi in gioco:
int main() { // ... auto addFn = Delegate(add); auto staticAddFnMem = Delegate(Math::add); auto diffFnMem = Delegate(&math, &Math::diff); auto addOpAsFnMem = Delegate(&addOp, &AddOp::operator()); auto addOpAsFnObj = Delegate(addOp); auto lambda = Delegate(add_); auto lambda2 = Delegate([](int a, int b) { return a + b; }); // ... return 0; }
Poiché il template argument deduction non si applica alle classi template, ma solo alle funzioni, per elimiare la specifica dei tipi bisogna ricorrere ad una funzione template ausiliaria; per esempio, per attivare il template argument deduction per le funzioni e i metodi statici è sufficiente introdurre una nuova funzione make_delegate così definita:
// ... template<typename R, typename... Args> Delegate<R, Args...> make_delegate(R(*fn)(Args...)) { return Delegate<R, Args...>(fn); } int main() { // wrapping a free function auto addFn = make_delegate(add); assert(addFn(1, 2) == 3); // wrapping a static function member auto staticAddFnMem = make_delegate(Math::add); assert(staticAddFnMem(1, 2) == 3); return 0; }
Per i metodi non statici occorre passare a make_delegate un riferimento all'istanza sulla quale invocare il metodo:
// ... template<class T, typename R, typename... Args> Delegate<R, Args...> make_delegate(T* obj, R(T::*fn)(Args...)) { return Delegate<R, Args...>(obj, fn); } int main() { // wrapping a free function auto addFn = make_delegate(add); assert(addFn(1, 2) == 3); // wrapping a static function member auto staticAddFnMem = make_delegate(Math::add); assert(staticAddFnMem(1, 2) == 3); // wrapping a non-static function member Math math; auto diffFnMem = make_delegate(&math, &Math::diff); assert(diffFnMem(3, 2) == 1); return 0; }
Per le lambda e i function object il problema consiste nel recuperare la signature dell'operatore function-call:
// ... template<typename R, typename... Args> Delegate<R, Args...> make_delegate(std::function<R(Args...)> fn) { return Delegate<R, Args...>(fn); } int main() { // ... // wrapping a function object /2 AddOp addOp; auto addOpAsFnObj = make_delegate(addOp); // compiler error! assert(addOpAsFnObj(1, 2) == 3); return 0; }
Il problema è risolvibile in due passi; introducendo dapprima un oggetto traits che ricava la signature dell'operatore function-call; in secondo luogo, definendo una funzione apposita, to_function, che effettua un cast esplicito dell'oggetto funzione a std::function, che il codice fin qui sviluppato è già in grado di trattare:
// ... template<class F> struct function_traits; // function signature capture template<class R, class... Args> struct function_traits<R(Args...)> { typedef R (*signature)(Args...); }; // function-call operator signature capture template <typename FnObj> struct function_traits : public function_traits<decltype(&FnObj::operator())> { }; // non-static method signature capture template <typename T, typename R, typename... Args> struct function_traits<R(T::*)(Args...)> { typedef std::function<R(Args...)> signature; }; template <typename FnObj> typename function_traits<FnObj>::signature to_function (FnObj fnObj) { return static_cast<typename function_traits<FnObj>::signature>(fnObj); } int main() { // ... // wrapping a function object /2 AddOp addOp; auto addOpAsFnObj = make_delegate(to_function(addOp)); assert(addOpAsFnObj(1, 2) == 3); return 0; }
to_function prende fnObj per valore anziché per referenza perché altrimenti le lambda passate per r-value reference non sarebbero supportate.
La soluzione è vicina; le lambda tuttavia non sono ancora convertibili a delegate, nemmeno attraverso l'applicazione di to_function, perché l'infrastruttura non supporta gli operator() const:
// ... int main() { // ... // wrapping a lambda auto add_ = [](int a, int b) { return a + b; }; auto lambda = make_delegate(to_function(add_)); // compiler error! assert(lambda(1, 2) == 3); return 0; }
Una nuova versione di make_delegate che accetta un puntatore a un metodo const risolve la questione:
// ... template <typename T, typename R, typename... Args> struct function_traits<R(T::*)(Args...) const> { typedef std::function<R(Args...)> signature; }; int main() { // ... // wrapping a lambda auto add_ = [](int a, int b) { return a + b; }; auto lambda = make_delegate(to_function(add_)); assert(lambda(1, 2) == 3); return 0; }
Il problema del const viene così risolto sull'operatore function call, ma permane sugli altri metodi:
// ... struct Math { static int add(int a, int b) { return a + b; } int diff(int a, int b) { return a - b; } int cdiff(int a, int b) const { return a - b; } }; struct ConstAddOp { int operator()(int a, int b) const { return a + b; } }; int main() { // ... // wrapping a const function object ConstAddOp constAddOp; auto constAddOpAsFnObj = make_delegate(to_function(constAddOp)); assert(constAddOpAsFnObj(1, 2) == 3); // wrapping a const non-static function member auto cdiffFnMem = make_delegate(&math, &Math::cdiff); // compiler error! assert(cdiffFnMem(3, 2) == 1); return 0; }
Per rendere compilabile il codice sopra riportato è necessario intervenire in due punti, introducendo una nuova versione const di make_delegate operante sui metodi non statici, oltre che sulla classe Delegate:
// ... template<typename R, typename... Args> class Delegate { // ... template<class T> struct ConstFnMemCall { const T* obj_; typedef R (T::*Fn)(Args...) const; Fn fn_; ConstFnMemCall(const T* obj, Fn fn) : obj_(obj), fn_(fn) { } R operator()(Args... args) const { return (obj_->*fn_)(args...); } }; public: // ... template<class T> Delegate(T* obj, R (T::*fn)(Args...) const) : fn_(ConstFnMemCall<T>(obj, fn)) { } // ... }; template<class T, typename R, typename... Args> Delegate<R, Args...> make_delegate(T* obj, R(T::*fn)(Args...) const) { return Delegate<R, Args...>(obj, fn); }
È possibile eliminare anche la chiamata esplicita a to_function, inglobandola direttamente in make_delegate, dopo averla modificata in modo da renderla applicabile anche ai puntatori a funzione:
// ... // function pointer signature capture template<class R, class... Args> struct function_traits<R(*)(Args...)> : public function_traits<R(Args...)> { }; template<class Fn> auto make_delegate(Fn fn) -> decltype(make_delegate(to_function(fn))) { return make_delegate(to_function(fn)); } int main() { auto addFn = make_delegate(add); assert(addFn(1, 2) == 3); // wrapping a static function member auto staticAddFnMem = make_delegate(Math::add); assert(staticAddFnMem(1, 2) == 3); // wrapping a non-static function member Math math; auto diffFnMem = make_delegate(&math, &Math::diff); assert(diffFnMem(3, 2) == 1); // wrapping a function object /2 AddOp addOp; auto addOpAsFnObj = make_delegate(to_function(addOp)); assert(addOpAsFnObj(1, 2) == 3); // wrapping a lambda auto add_ = [](int a, int b) { return a + b; }; auto lambda = make_delegate(to_function(add_)); assert(lambda(1, 2) == 3); // wrapping a temporary lambda auto lambda2 = make_delegate(to_function([](int a, int b) { return a + b; })); assert(lambda2(1, 2) == 3); // wrapping a const function object ConstAddOp constAddOp; auto constAddOpAsFnObj = make_delegate(to_function(constAddOp)); assert(constAddOpAsFnObj(1, 2) == 3); // wrapping a const non-static function member auto cdiffFnMem = make_delegate(&math, &Math::cdiff); assert(cdiffFnMem(3, 2) == 1); }
Non ho ancora avuto modo di impiegare i delegate in progetti reali. Durante lo sviluppo, tuttavia, è emersa la necessità di esplicitare i tipi in gioco nel momento in cui si intende catturare una funzione overloaded, cioè in tutti i casi di omonimia:
// ... int overloaded(double /*x*/) { return 0; } int overloaded(const std::string& /*s*/) { return 1; } int main() { // ... /* won't compile * auto overloaded1 = make_delegate(f); */ auto overloaded2 = make_delegate(static_cast<int (*)(double)>(overloaded)); assert(overloaded2(3.14) == 0); auto overloaded3 = make_delegate(static_cast<int (*)(const std::string&)>(overloaded)); assert(overloaded3("acme") == 1); return 0; }
Pagina modificata il 18/08/2014