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